Changes to ScrollHub

Breck Yunits
Breck Yunits
14 hours ago
public/scrollHubEditor.js
Changed around line 358: class EditorApp {
- "ds_store thumbs.db desktop.ini pdf png jpg jpeg gif webp bmp tiff ico svg eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
+ "ds_store thumbs.db pdf png jpg jpeg gif webp bmp tiff ico eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
Breck Yunits
Breck Yunits
15 hours ago
package.json
Changed around line 1
- "version": "0.82.0",
+ "version": "0.83.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.83.0 1/21/2025
+ 🎉 handle binary files better
+
Breck Yunits
Breck Yunits
15 hours ago
Better binary file handling
public/scrollHubEditor.js
Changed around line 309: class EditorApp {
- const response = await fetch(`/readFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(filePath)}`)
- const content = await response.text()
- this.setFileContent(content)
+ // Update UI state for binary files
+ if (this.isBinaryFile(fileName)) {
+ this.setFileContent("Binary file not shown.")
+ this.codeMirrorInstance.setOption("readOnly", true)
+ this.updateUIForBinaryFile(true)
+ } else {
+ // Regular file handling
+ const response = await fetch(`/readFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(filePath)}`)
+ const content = await response.text()
+ this.setFileContent(content)
+ this.codeMirrorInstance.setOption("readOnly", false)
+ this.updateUIForBinaryFile(false)
+ await this.refreshParserCommand()
+ this.updateEditorMode(this.getEditorMode(fileName))
+ }
+
- await this.refreshParserCommand()
- this.updateEditorMode(this.getEditorMode(fileName))
Changed around line 355: class EditorApp {
+ // Add a method to check if a file is binary
+ isBinaryFile(fileName) {
+ const binaryExtensions = new Set(
+ "ds_store thumbs.db desktop.ini pdf png jpg jpeg gif webp bmp tiff ico svg eps raw cr2 nef heic doc docx xls xlsx ppt pptx odt ods odp pages numbers key zip tar gz 7z rar bz2 dmg iso tgz exe dll so dylib bin app msi deb rpm mp3 wav ogg mp4 avi mov wmv flv mkv".split(
+ " "
+ )
+ )
+ const extension = fileName.split(".").pop().toLowerCase()
+ return binaryExtensions.has(extension)
+ }
+
+ updateUIForBinaryFile(isBinary) {
+ // Get UI elements
+ const saveButton = document.querySelector('[onclick*="saveAndPublishCommand"]')
+ const formatButton = document.querySelector('[onclick*="formatFileCommand"]')
+
+ if (saveButton) {
+ saveButton.style.display = isBinary ? "none" : "inline-block"
+ }
+ if (formatButton) {
+ formatButton.style.display = isBinary ? "none" : "inline-block"
+ }
+
+ // Update editor styling for binary files
+ const editorElement = this.codeMirrorInstance.getWrapperElement()
+ if (isBinary) {
+ editorElement.classList.add("binary-file")
+ this.codeMirrorInstance.setOption("lineNumbers", false)
+ this.codeMirrorInstance.refresh()
+ } else {
+ editorElement.classList.remove("binary-file")
+ this.codeMirrorInstance.setOption("lineNumbers", this.showLineNumbers)
+ this.codeMirrorInstance.refresh()
+ }
+ }
+
public/scrollHubStyle.css
Changed around line 385: textarea {
+
+ .binary-file .CodeMirror-lines {
+ background-color: #f5f5f5;
+ color: #666;
+ font-style: italic;
+ cursor: not-allowed;
+ }
+ .binary-file .CodeMirror-cursor {
+ display: none !important;
+ }
Breck Yunits
Breck Yunits
15 hours ago
Only load in preview iframe when it makes sense
public/edit.scroll
Changed around line 12: libs.js
-
+
public/scrollHubEditor.js
Changed around line 861: I'd love to hear your requests and feedback! Find me on Warpcast.
+ get isPreviewableFile() {
+ if (!this.fileName) return false
+ const previewableExtensions = "html htm scroll parsers md txt css svg png jpg jpeg gif webp pdf".split(" ")
+ return previewableExtensions.some(ext => this.fileName.toLowerCase().endsWith(ext))
+ }
+
- this.previewIFrame.src = this.permalink
+
+ if (this.isPreviewableFile) this.previewIFrame.src = this.permalink
+ else this.previewIFrame.src = "about:blank"
Breck Yunits
Breck Yunits
16 hours ago
add server status route
ScrollHub.js
Changed around line 190: class ScrollHub {
+ this.requestsServed = 0
+ this.startCpuUsage = process.cpuUsage()
+ this.lastCpuUsage = this.startCpuUsage
+ this.lastCpuCheck = Date.now()
Changed around line 515: If you'd like to create this folder, visit our main site to get started.
+ this.requestsServed++
Changed around line 1320: If you'd like to create this folder, visit our main site to get started.
+ getCpuUsagePercent() {
+ const currentCpuUsage = process.cpuUsage(this.lastCpuUsage)
+ const currentTime = Date.now()
+ const timeDiff = currentTime - this.lastCpuCheck
+
+ // Calculate CPU usage percentage
+ const totalTicks = (currentCpuUsage.user + currentCpuUsage.system) / 1000 // Convert to microseconds
+ const cpuPercent = (totalTicks / timeDiff) * 100
+
+ // Update tracking variables
+ this.lastCpuUsage = process.cpuUsage()
+ this.lastCpuCheck = currentTime
+
+ return Math.min(100, cpuPercent.toFixed(1))
+ }
+
Changed around line 1363: If you'd like to create this folder, visit our main site to get started.
+ // Add the status route (in initCommandRoutes or as a separate method)
+ app.get("/status.htm", async (req, res) => {
+ const uptime = Math.floor((Date.now() - this.startTime) / 1000)
+ const memUsage = process.memoryUsage()
+ const totalMem = os.totalmem()
+ const freeMem = os.freemem()
+ const cpuUsage = this.getCpuUsagePercent()
+ const loadAvg = os.loadavg()
+
+ const status = {
+ server: {
+ version: this.version,
+ uptime: {
+ seconds: uptime,
+ formatted: `${Math.floor(uptime / 86400)}d ${Math.floor((uptime % 86400) / 3600)}h ${Math.floor((uptime % 3600) / 60)}m ${uptime % 60}s`
+ },
+ startTime: new Date(this.startTime).toISOString(),
+ hostname: this.hostname,
+ platform: process.platform,
+ nodeVersion: process.version
+ },
+ performance: {
+ cpu: {
+ usage: `${cpuUsage}%`,
+ cores: os.cpus().length,
+ loadAverage: {
+ "1m": loadAvg[0].toFixed(2),
+ "5m": loadAvg[1].toFixed(2),
+ "15m": loadAvg[2].toFixed(2)
+ }
+ },
+ memory: {
+ total: `${(totalMem / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ free: `${(freeMem / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ used: `${((totalMem - freeMem) / 1024 / 1024 / 1024).toFixed(2)} GB`,
+ process: {
+ heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
+ heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
+ rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`
+ }
+ }
+ },
+ activity: {
+ requestsServed: this.requestsServed,
+ requestsPerSecond: (this.requestsServed / uptime).toFixed(2),
+ activeFolders: Object.keys(this.folderCache).length,
+ buildRequests: Object.keys(this.buildRequests).length,
+ sseClients: this.sseClients.size
+ }
+ }
+
+ const particle = new Particle(status)
+ particle.topDownArray.forEach(particle => particle.setCue("- " + particle.cue))
+ const html = await ScrollToHtml(particle.toString())
+
+ res.send(html)
+ })
+
Breck Yunits
Breck Yunits
17 hours ago
build speed improvements
ScrollHub.js
Changed around line 1342: If you'd like to create this folder, visit our main site to get started.
- app.get("/build/:folderName", checkWritePermissions, async (req, res) => {
- await this.runScrollCommand(req, res, "build")
- this.updateFolderAndBuildList(this.getFolderName(req))
- })
+ const buildFolder = async (req, res) => {
+ const now = Date.now()
+ const folderName = this.getFolderName(req)
+ if (!this.folderCache[folderName]) return res.status(404).send("Folder not found")
+ await this.buildFolder(folderName)
+ res.send((Date.now() - now).toString())
+ this.updateFolderAndBuildList(folderName)
+ }
- app.post("/build.htm", checkWritePermissions, async (req, res) => {
- await this.runScrollCommand(req, res, "build")
- this.updateFolderAndBuildList(this.getFolderName(req))
- })
+ app.get("/build/:folderName", checkWritePermissions, buildFolder)
+ app.post("/build.htm", checkWritePermissions, buildFolder)
+
+ app.get("/building.htm", async (req, res) => res.send(JSON.stringify(this.buildRequests)))
Changed around line 1754: If you'd like to create this folder, visit our main site to get started.
+ // todo: keep some folders in memory
- this.buildRequests[folderName] = 0
+ delete this.buildRequests[folderName]
public/scrollHubEditor.js
Changed around line 406: class EditorApp {
-
- console.log(`'${this.folderName}' built`)
+ console.log(`'${this.folderName}' built in ${message}ms`)
Breck Yunits
Breck Yunits
18 hours ago
Update release notes
package.json
Changed around line 1
- "version": "0.81.0",
+ "version": "0.82.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.82.0 1/21/2025
+ 🎉 ability to set Git author
+ 🎉 upgraded Scroll
+ 🎉 better diff UI
+ 🎉 persist hidden files setting
+ 🎉 add deepseek reasoner model
+ 🎉 better prompt logging
+ 🎉 prettier format json
+ 🎉 create folder by just uploading files
+ 🏥 dont attempt to make ssl certs for non-existant folders
+ 🏥 fix commits.json route
+ 🏥 fix mjs file serving
+
Breck Yunits
Breck Yunits
18 hours ago
add ability to set git author
ScrollHub.js
Changed around line 654: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git checkout ${targetHash} . && git add . && git commit --author="${clientIp} <${clientIp}@${hostname}>" -m "Reverted to ${targetHash}" --allow-empty`, { cwd: folderPath })
+ await execAsync(`git checkout ${targetHash} . && git add . && git commit ${this.getCommitAuthor(req)} -m "Reverted to ${targetHash}" --allow-empty`, { cwd: folderPath })
Changed around line 669: If you'd like to create this folder, visit our main site to get started.
+ getCommitAuthor(req) {
+ let author = ""
+ if (req.body.author?.match(/^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$/)) author = req.body.author
+ else {
+ const clientIp = req.ip || req.connection.remoteAddress
+ const hostname = req.hostname?.toLowerCase()
+ const author = `${clientIp} <${clientIp}@${hostname}>`
+ }
+ return `--author="${author}"`
+ }
+
Changed around line 1167: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git add "${fileName}"; git commit --author="${clientIp} <${clientIp}@${hostname}>" -m 'Inserted particles into ${fileName}'`, { cwd: folderPath })
+ await execAsync(`git add "${fileName}"; git commit ${this.getCommitAuthor(req)} -m 'Inserted particles into ${fileName}'`, { cwd: folderPath })
Changed around line 1185: If you'd like to create this folder, visit our main site to get started.
- const filePath = path.join(rootFolder, folderName, decodeURIComponent(req.query.filePath))
+ const filePath = path.join(rootFolder, folderName, req.body.filePath)
Changed around line 1197: If you'd like to create this folder, visit our main site to get started.
- await execAsync(`git rm ${fileName}; git commit -m 'Deleted ${fileName}'`, { cwd: folderPath })
+ await execAsync(`git rm ${fileName}; git commit -m 'Deleted ${fileName}' ${this.getCommitAuthor(req)}`, { cwd: folderPath })
Changed around line 1252: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- await execAsync(`git mv ${oldFileName} ${newFileName}; git commit --author="${clientIp} <${clientIp}@${hostname}>" -m 'Renamed ${oldFileName} to ${newFileName}'`, { cwd: folderPath })
+ await execAsync(`git mv ${oldFileName} ${newFileName}; git commit ${this.getCommitAuthor(req)} -m 'Renamed ${oldFileName} to ${newFileName}'`, { cwd: folderPath })
Changed around line 1658: If you'd like to create this folder, visit our main site to get started.
- const clientIp = req.ip || req.connection.remoteAddress
- const hostname = req.hostname?.toLowerCase()
- const author = `${clientIp} <${clientIp}@${hostname}>`
+ const author = this.getCommitAuthor(req)
Changed around line 1684: If you'd like to create this folder, visit our main site to get started.
- await execAsync(`git add -f ${relativePath}; git commit --author="${author}" -m '${action} ${relativePath}'`, { cwd: folderPath })
+ await execAsync(`git add -f ${relativePath}; git commit ${author} -m '${action} ${relativePath}'`, { cwd: folderPath })
public/scrollHubEditor.js
Changed around line 302: class EditorApp {
- this.updateFooterLinks()
+ await this.updateFooterLinks()
Changed around line 379: class EditorApp {
- const { folderName } = this
+ const { folderName, author } = this
- const formData = new FormData()
- formData.append("filePath", filePath)
- formData.append("folderName", folderName)
- formData.append("content", content)
- const response = await fetch("/writeFile.htm", {
- method: "POST",
- body: formData
- })
+ const response = await this.postData("/writeFile.htm", { filePath, content })
Changed around line 395: class EditorApp {
- const formData = new FormData()
- const { folderName } = this
- formData.append("folderName", folderName)
- const response = await fetch("/build.htm", {
- method: "POST",
- body: formData
- })
+ const response = await this.postData("/build.htm")
Changed around line 407: class EditorApp {
- console.log(`'${folderName}' built`)
+ console.log(`'${this.folderName}' built`)
Changed around line 834: I'd love to hear your requests and feedback! Find me on Warpcast.
- const formData = new FormData()
- formData.append("file", file)
- formData.append("folderName", this.folderName)
-
- const response = await fetch("/uploadFile.htm", {
- method: "POST",
- body: formData
- })
-
+ const response = await this.postData("/uploadFile.htm", { file })
Changed around line 915: I'd love to hear your requests and feedback! Find me on Warpcast.
- // Update the updateFooterLinks method to use the new modal
- updateFooterLinks() {
+ async updateFooterLinks() {
- document.getElementById("folderLinks").innerHTML =
+ const code = `a traffic
+ class folderActionLink
+ href /globe.html?folderName=${folderName}
+ onclick if (!event.ctrlKey && !event.metaKey) { window.app.openIframeModalFromClick(event); return false; }
+ span ·
+ a revisions
+ class folderActionLink
+ href /commits.htm?folderName=${folderName}&count=30
+ onclick if (!event.ctrlKey && !event.metaKey) { window.app.openIframeModalFromClick(event); return false; }
+ span ·
+ a clone
+ class folderActionLink
+ href #
+ onclick window.app.copyClone()
+ span ·
+ a download
+ class folderActionLink
+ href ${folderName}.zip
+ span ·
+ a duplicate
+ class folderActionLink
+ href #
+ onclick window.app.duplicate()
+ span ·
+ a move
+ href #
+ class folderActionLink
+ onclick window.app.renameFolder()
+ span ·
+ a delete
+ href #
+ class folderActionLink
+ onclick window.app.deleteFolder()
+ span ·
+ a ${this.authorDisplayName}
+ href #
+ class folderActionLink
+ linkify false
+ onclick window.app.loginCommand()`
+ const html = await this.fusionEditor.scrollToHtml(code)
+ document.getElementById("folderLinks").innerHTML = html
+ }
+
+ get author() {
+ return localStorage.getItem("author")
+ }
+
+ get authorDisplayName() {
+ const { author } = this
+ if (!author) return "anon"
+ return author.split("<")[1].split(">")[0]
+ }
+
+ async loginCommand() {
+ const { author } = this
+ const defaultAuthor = "Anon "
+ const newAuthorName = prompt(`Your name and email:`, author || defaultAuthor)
+ if (newAuthorName === "" || newAuthorName === defaultAuthor) {
+ localStorage.removeItem("author", undefined)
+ } else if (newAuthorName && newAuthorName.match(/^[^<>]+\s<[^<>@\s]+@[^<>@\s]+>$/)) {
+ localStorage.setItem("author", newAuthorName)
+ }
+
+ await this.updateFooterLinks()
- const response = await fetch("/mv.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `oldFolderName=${this.folderName}&newFolderName=${newFolderName}`
- })
-
+ const response = await this.postData("/mv.htm", { oldFolderName: this.folderName, newFolderName })
Changed around line 997: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch("/trashFolder.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${encodeURIComponent(this.folderName)}`
- })
-
+ const response = await this.postData("/trashFolder.htm")
Changed around line 1008: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch("/cloneFolder.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${this.folderName}&redirect=false`
- })
-
+ const response = await this.postData("/cloneFolder.htm", { redirect: "false" })
Changed around line 1104: I'd love to hear your requests and feedback! Find me on Warpcast.
+ async postData(url, params = {}) {
+ const { folderName, author } = this
+ const formData = new FormData()
+ formData.append("folderName", folderName)
+ if (author) formData.append("author", author)
+ Object.keys(params).forEach(key => {
+ formData.append(key, params[key])
+ })
+ const response = await fetch(url, {
+ method: "POST",
+ body: formData
+ })
+ return response
+ }
+
- const response = await fetch("/renameFile.htm", {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded"
- },
- body: `folderName=${encodeURIComponent(folderName)}&oldFileName=${encodeURIComponent(oldFileName)}&newFileName=${encodeURIComponent(newFileName)}`
- })
-
+ const response = await this.postData("/renameFile.htm", { oldFileName, newFileName })
Changed around line 1197: I'd love to hear your requests and feedback! Find me on Warpcast.
- const response = await fetch(`/deleteFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(fileName)}`, {
- method: "POST"
- })
-
+ const response = await this.postData("/deleteFile.htm", { filePath: fileName })
Breck Yunits
Breck Yunits
1 day ago
better diff styling
FolderIndex.js
Changed around line 1
- const AnsiToHtml = require("ansi-to-html")
Changed around line 180: class FolderIndex {
- // Get detailed commit information with a specific format
+ // Get detailed commit information with a custom format that's easier to parse
- const gitLogProcess = spawn("git", ["log", `-${count}`, "--color=always", "--date=iso", "--format=commit %H%nAuthor: %an <%ae>%nDate: %ad%nSubject: %s%n%n%b%n", "-p"], { cwd: folderPath })
+ const gitLogProcess = spawn("git", ["log", `-${count}`, "--color=always", "--date=iso", "--format=COMMIT_START%n%H%n%an%n%ae%n%ad%n%B%nCOMMIT_DIFF_START%n", "-p"], { cwd: folderPath })
Changed around line 200: class FolderIndex {
- const commitChunks = logOutput.split(/(?=commit [0-9a-f]{40}\n)/)
+ const commitChunks = logOutput.split("COMMIT_START\n").filter(Boolean)
- if (!chunk.trim()) continue
-
- const commitMatch = chunk.match(/commit (\w+)\n/)
- const authorMatch = chunk.match(/Author: ([^<]+)<([^>]+)>/)
- const dateMatch = chunk.match(/Date:\s+(.+)\n/)
- const messageMatch = chunk.match(/\n\n\s+(.+?)\n/)
- const diffContent = chunk.split(/\n\n/)[2] || ""
-
- if (commitMatch && authorMatch && dateMatch) {
- commits.push({
- id: commitMatch[1],
- name: authorMatch[1].trim(),
- email: authorMatch[2].trim(),
- time: new Date(dateMatch[1]),
- message: messageMatch ? messageMatch[1].trim() : "",
- diff: diffContent,
- rawOutput: chunk // Keep raw output for HTML generation
- })
+ const [commitInfo, ...diffParts] = chunk.split("COMMIT_DIFF_START\n")
+ const [hash, name, email, date, ...messageLines] = commitInfo.split("\n")
+
+ // Remove any trailing empty lines from the message
+ while (messageLines.length > 0 && messageLinesessageLines.length - 1].trim() === "") {
+ messageLines.pop()
+
+ // Join the message lines back together to preserve formatting
+ const message = messageLines.join("\n").trim()
+ const diff = diffParts.join("COMMIT_DIFF_START\n") // Restore any split diff parts
+
+ commits.push({
+ id: hash,
+ name: name.trim(),
+ email: email.trim(),
+ time: new Date(date),
+ message,
+ diff
+ })
Changed around line 232: class FolderIndex {
+ formatTimeAgo(date) {
+ const seconds = Math.floor((new Date() - date) / 1000)
+ const minutes = Math.floor(seconds / 60)
+ const hours = Math.floor(minutes / 60)
+ const days = Math.floor(hours / 24)
+ const months = Math.floor(days / 30)
+ const years = Math.floor(months / 12)
+
+ if (years > 0) return `${years} ${years === 1 ? "year" : "years"} ago`
+ if (months > 0) return `${months} ${months === 1 ? "month" : "months"} ago`
+ if (days > 0) return `${days} ${days === 1 ? "day" : "days"} ago`
+ if (hours > 0) return `${hours} ${hours === 1 ? "hour" : "hours"} ago`
+ if (minutes > 0) return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`
+ return `${seconds} ${seconds === 1 ? "second" : "seconds"} ago`
+ }
+
+ stripAnsi(text) {
+ // Remove ANSI escape codes
+ return text
+ .replace(/\u001b\[\d+m/g, "")
+ .replace(/\u001b\/g, "")
+ .replace(/\[\d+m/g, "")
+ .replace(/\/g, "")
+ }
+
+ parseDiff(diffText) {
+ const files = []
+ let currentFile = null
+
+ // Clean the diff text of ANSI codes first
+ const cleanDiff = this.stripAnsi(diffText)
+ const lines = cleanDiff.split("\n")
+
+ for (const line of lines) {
+ const cleanLine = line.trim()
+ if (!cleanLine) continue
+
+ if (cleanLine.startsWith("diff --git")) {
+ if (currentFile) files.push(currentFile)
+ const fileMatch = cleanLine.match(/b\/(.*?)(\s+|$)/)
+ const filename = fileMatch ? fileMatch[1] : "unknown file"
+ currentFile = { filename, changes: [] }
+ } else if (cleanLine.startsWith("+++") || cleanLine.startsWith("---") || cleanLine.startsWith("index")) {
+ continue // Skip these technical git lines
+ } else if (cleanLine.startsWith("+") && !cleanLine.startsWith("+++")) {
+ currentFile?.changes.push({ type: "addition", content: cleanLine.substring(1) })
+ } else if (cleanLine.startsWith("-") && !cleanLine.startsWith("---")) {
+ currentFile?.changes.push({ type: "deletion", content: cleanLine.substring(1) })
+ } else if (cleanLine.startsWith("@@")) {
+ const match = cleanLine.match(/@@ -(\d+),?\d* \+(\d+),?\d* @@(.*)/)
+ if (match) {
+ const contextInfo = match[3].trim()
+ currentFile?.changes.push({
+ type: "info",
+ content: contextInfo ? `Changed around line ${match[2]}: ${contextInfo}` : `Changed around line ${match[2]}`
+ })
+ }
+ }
+ }
+ if (currentFile) files.push(currentFile)
+ return files
+ }
+
+ commitToHtml(commit, folderName) {
+ const timeAgo = this.formatTimeAgo(commit.time)
+ const files = this.parseDiff(commit.diff)
+
+ let html = `
+
+
+
+ + .trim()
+ .toLowerCase()
+ .split("")
+ .reduce((hash, char) => {
+ const chr = char.charCodeAt(0)
+ return ((hash << 5) - hash + chr) >>> 0
+ }, 0)}?s=40&d=identicon" alt="${commit.name}" class="avatar">
+
+
${commit.name}
+
${timeAgo}
+
+
+
+
+
+
+
+
+
+
+ Restore this version
+
+
+
+
+
+
${commit.message}
+
+
+ `
+
+ for (const file of files) {
+ html += `
+
+
+
+
+
+ ${file.filename}
+
+
+ `
+
+ for (const change of file.changes) {
+ if (change.type === "info") {
+ html += `
${change.content}
`
+ } else if (change.type === "addition") {
+ html += `
+ ${change.content}
`
+ } else if (change.type === "deletion") {
+ html += `
- ${change.content}
`
+ }
+ }
+
+ html += `
+
+
+ `
+ }
+
+ html += `
+
+
+ `
+
+ return html
+ }
+
- res.status(200).send(`No commits available for ${folderName}.`)
+ res.status(200).send(`No changes have been made to ${folderName} yet.`)
- const convert = new AnsiToHtml({ escapeXML: true })
-
- // Send HTML header
Changed around line 387: class FolderIndex {
- Last ${count} Commits for ${folderName}
+ Changes to ${folderName}
- body { font-family: monospace; white-space: pre-wrap; word-wrap: break-word; padding: 5px; }
- h2 { color: #333; }
- .aCommit {background-color: rgba(238, 238, 238, 0.8); padding: 8px; border-radius: 3px; margin-bottom: 10px;}
- .commit { border-bottom: 1px solid #ccc; padding-bottom: 20px; margin-bottom: 20px; }
- .commit-message { font-weight: bold; color: #005cc5; }
- input[type="submit"] { font-size: 0.8em; padding: 2px 5px; margin-left: 10px; }
+ :root {
+ --color-bg: #ffffff;
+ --color-text: #24292f;
+ --color-border: #d0d7de;
+ --color-addition-bg: #e6ffec;
+ --color-deletion-bg: #ffebe9;
+ --color-info-bg: #f6f8fa;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --color-bg: #0d1117;
+ --color-text: #c9d1d9;
+ --color-border: #30363d;
+ --color-addition-bg: #0f2f1a;
+ --color-deletion-bg: #2d1214;
+ --color-info-bg: #161b22;
+ }
+ }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ color: var(--color-text);
+ background: var(--color-bg);
+ margin: 0;
+ padding: 20px;
+ }
+
+ .container {
+ max-width: 900px;
+ margin: 0 auto;
+ }
+
+ .header {
+ margin-bottom: 30px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .header h1 {
+ font-size: 24px;
+ margin: 0;
+ }
+
+ .header h1 a{
+ color: var(--color-text);
+ text-decoration-color: transparent;
+ }
+
+ .header h1 a:hover{
+ color: var(--color-text);
+ text-decoration-color: var(--color-text);
+ }
+
+ .commit {
+ background: var(--color-bg);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ margin-bottom: 24px;
+ overflow: hidden;
+ }
+
+ .commit-header {
+ padding: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .commit-author {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ }
+
+ .avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ }
+
+ .author-name {
+ font-weight: 600;
+ }
+
+ .commit-time {
+ color: #57606a;
+ font-size: 12px;
+ }
+
+ .restore-button {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ border-radius: 6px;
+ border: 1px solid var(--color-border);
+ background: var(--color-bg);
+ color: var(--color-text);
+ cursor: pointer;
+ font-size: 12px;
+ transition: all 0.2s;
+ }
+
+ .restore-button:hover {
+ background: var(--color-info-bg);
+ }
+
+ .commit-message {
+ padding: 16px;
+ font-size: 14px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ .commit-details {
+ padding: 16px;
+ }
+
+ .file-change {
+ margin-bottom: 24px;
+ }
+
+ .file-change:last-child {
+ margin-bottom: 0;
+ }
+
+ .filename {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 12px;
+ margin-bottom: 8px;
+ }
+
+ .changes {
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+ font-size: 12px;
+ line-height: 1.5;
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ overflow: hidden;
+ }
+
+ .line {
+ padding: 4px 8px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+
+ .addition {
+ background: var(--color-addition-bg);
+ }
+
+ .deletion {
+ background: var(--color-deletion-bg);
+ }
+
+ .change-info {
+ padding: 4px 8px;
+ background: var(--color-info-bg);
+ color: #57606a;
+ font-size: 12px;
+ border-bottom: 1px solid var(--color-border);
+ }
+
+ @media (max-width: 600px) {
+ .commit-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 16px;
+ }
+
+ .restore-button {
+ width: 100%;
+ justify-content: center;
+ }
+ }
+
- // Process each commit
- let output = commit.rawOutput
- // Convert ANSI color codes to HTML
- output = convert.toHtml(output)
- // Add restore version button
- output = output.replace(/(commit\s)([0-9a-f]{40})/, (match, prefix, hash) => {
- return `
- ${prefix}${hash}
-
-
-
- `
- })
- res.write(output + "
\n")
+ res.write(this.commitToHtml(commit, folderName))
- res.end("")
+ res.end(`
+
+
+ `)
- res.status(500).send("An error occurred while fetching the git log")
+ res.status(500).send("Sorry, we couldn't load the change history right now. Please try again.")
-
package.json
Changed around line 7
- "ansi-to-html": "^0.7.2",
public/scrollHubEditor.js
Changed around line 940: I'd love to hear your requests and feedback! Find me on Warpcast.
Breck Yunits
Breck Yunits
1 day ago
ScrollHub.js
Changed around line 640: If you'd like to create this folder, visit our main site to get started.
+ res.send(JSON.stringify(commits, undefined, 2))
Breck Yunits
Breck Yunits
1 day ago
persist hidden files setting
public/scrollHubEditor.js
Changed around line 611: nokey1 showWelcomeMessageCommand Help`
- this.showHiddenFiles = !this.showHiddenFiles
+ this.setFromLocalOrMemory("showHiddenFiles", !this.showHiddenFiles)
+ get showHiddenFiles() {
+ return this.getFromLocalOrMemory("showHiddenFiles") === "true"
+ }
+
+ setFromLocalOrMemory(key, value) {
+ try {
+ localStorage.setItem(key, value)
+ } catch (err) {
+ this[key] = value
+ console.error(err)
+ }
+ }
+
+ getFromLocalOrMemory(key) {
+ try {
+ return localStorage.getItem(key)
+ } catch (err) {
+ console.error(err)
+ return this[key]
+ }
+ }
+
Changed around line 998: I'd love to hear your requests and feedback! Find me on Warpcast.
- showHiddenFiles = false
Breck Yunits
Breck Yunits
1 day ago
package.json
Changed around line 19
- "scroll-cli": "^168.6.0",
+ "scroll-cli": "^168.8.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlMethodAtom\n paint constant\n enum get post\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nmimeTypeAtom\n extends stringAtom\n paint constant.language\n description File MIME types (e.g. 'image/*', 'application/pdf')\n enum image/* image/jpeg image/png image/gif image/webp image/svg+xml application/pdf text/plain text/html text/css text/javascript text/csv application/json application/xml application/zip application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio/* audio/mpeg audio/wav audio/ogg video/* video/mp4 video/webm video/ogg\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n description Set HTML attribute.\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMinParser\n cue min\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextMaxParser\n cue max\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextStepParser\n cue step\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextWidthParser\n cue width\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextHeightParser\n cue height\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextRoleParser\n popularity 0.000217\n cue role\n description Set ARIA role attribute for accessibility.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTabindexParser\n popularity 0.000217\n cue tabindex\n description Set tabindex attribute for keyboard navigation.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextColspanParser\n popularity 0.000217\n cue colspan\n description Set colspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextRowspanParser\n popularity 0.000217\n cue rowspan\n description Set rowspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextMethodParser\n popularity 0.000217\n cue method\n description Set form method attribute (GET/POST).\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlMethodAtom\naftertextActionParser\n popularity 0.000217\n cue action\n description Set form action URL.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextAutocompleteParser\n popularity 0.000217\n cue autocomplete\n description Set form autocomplete attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextMultipleParser\n popularity 0.000217\n cue multiple\n description Allow multiple selections or files.\n extends abstractAftertextAttributeParser\n single\naftertextSelectedParser\n popularity 0.000217\n cue selected\n description Set selected state for options.\n extends abstractAftertextAttributeParser\n single\naftertextCheckedParser\n popularity 0.000217\n cue checked\n description Set checked state for inputs.\n extends abstractAftertextAttributeParser\n single\naftertextRelParser\n popularity 0.000217\n cue rel\n description Set rel attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextEnctypeParser\n popularity 0.000217\n cue enctype\n description Set form enctype for file uploads.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextAcceptParser\n popularity 0.000217\n cue accept\n description Set accepted file types for file inputs.\n extends abstractAftertextAttributeParser\n catchAllAtomType mimeTypeAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlMethodAtom\n paint constant\n enum get post\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nmimeTypeAtom\n extends stringAtom\n paint constant.language\n description File MIME types (e.g. 'image/*', 'application/pdf')\n enum image/* image/jpeg image/png image/gif image/webp image/svg+xml application/pdf text/plain text/html text/css text/javascript text/csv application/json application/xml application/zip application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio/* audio/mpeg audio/wav audio/ogg video/* video/mp4 video/webm video/ogg\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nhtmlStrongParser\n extends abstractHtmlElementParser\n cue strong\n example\n strong Important text\nhtmlEmParser\n extends abstractHtmlElementParser\n cue em\n example\n em Emphasized text\nhtmlBlockquoteParser\n extends abstractHtmlElementParser\n cue blockquote\n example\n blockquote\n p A wise quote\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n description Set HTML attribute.\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMinParser\n cue min\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextMaxParser\n cue max\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextStepParser\n cue step\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextWidthParser\n cue width\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextHeightParser\n cue height\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextRoleParser\n popularity 0.000217\n cue role\n description Set ARIA role attribute for accessibility.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTabindexParser\n popularity 0.000217\n cue tabindex\n description Set tabindex attribute for keyboard navigation.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextColspanParser\n popularity 0.000217\n cue colspan\n description Set colspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextRowspanParser\n popularity 0.000217\n cue rowspan\n description Set rowspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextMethodParser\n popularity 0.000217\n cue method\n description Set form method attribute (GET/POST).\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlMethodAtom\naftertextActionParser\n popularity 0.000217\n cue action\n description Set form action URL.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextAutocompleteParser\n popularity 0.000217\n cue autocomplete\n description Set form autocomplete attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextMultipleParser\n popularity 0.000217\n cue multiple\n description Allow multiple selections or files.\n extends abstractAftertextAttributeParser\n single\naftertextSelectedParser\n popularity 0.000217\n cue selected\n description Set selected state for options.\n extends abstractAftertextAttributeParser\n single\naftertextCheckedParser\n popularity 0.000217\n cue checked\n description Set checked state for inputs.\n extends abstractAftertextAttributeParser\n single\naftertextRelParser\n popularity 0.000217\n cue rel\n description Set rel attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextEnctypeParser\n popularity 0.000217\n cue enctype\n description Set form enctype for file uploads.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextAcceptParser\n popularity 0.000217\n cue accept\n description Set accepted file types for file inputs.\n extends abstractAftertextAttributeParser\n catchAllAtomType mimeTypeAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.8.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
2 days ago
public/pageHeader.scroll
Changed around line 5: header.scroll
- nav page
+ fileNav page
Breck Yunits
Breck Yunits
2 days ago
Agents.js
Changed around line 75: ${domainExpression}`
- files["readme.scroll"] = `# ${finalDomain}\n\n${this.what} generated by ${this.agent.name} from prompt: ${this.userPrompt}`
+ files["readme.scroll"] = `# ${finalDomain}
+
+ Prompt: ${this.what}
+ Agent: ${this.agent.name}
+ Model: ${this.agent.model}
+
+ ## User prompt
+ ${this.userPrompt}
+
+ ## System prompt
+ ${this.systemPrompt}`
Changed around line 109: class Claude extends AbstractAgent {
- name = "Claude"
+ name = "claude"
+ model = "claude-3-5-sonnet-20241022"
- model: "claude-3-5-sonnet-20241022",
+ model: this.model,
Changed around line 138: class DeepSeek extends AbstractAgent {
- name = "DeepSeek"
+ name = "deepseek"
- messages: [{ role: "system", content: prompt.systemPrompt }],
+ messages: this.getMessages(prompt),
+
+ getMessages(prompt) {
+ return [{ role: "system", content: prompt.systemPrompt }]
+ }
- name = "DeepSeekReasoner"
+ name = "deepseekreasoner"
+
+ getMessages(prompt) {
+ return [
+ { role: "system", content: prompt.systemPrompt },
+ { role: "user", content: prompt.userPrompt }
+ ]
+ }
Changed around line 172: class Agents {
- this.availableAgents.forEach(agent => this.loadAgent(agent))
+ const availableAgents = "claude deepseek".split(" ")
+ availableAgents.forEach(agent => this.loadAgent(agent))
- availableAgents = "claude deepseek".split(" ")
-
Changed around line 189: class Agents {
- return (this.agents[agent.name] = agent)
+ this.agents[agent.name] = agent
Breck Yunits
Breck Yunits
2 days ago
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n description Set HTML attribute.\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMinParser\n cue min\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextMaxParser\n cue max\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextWidthParser\n cue width\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextHeightParser\n cue height\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.5.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlMethodAtom\n paint constant\n enum get post\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nmimeTypeAtom\n extends stringAtom\n paint constant.language\n description File MIME types (e.g. 'image/*', 'application/pdf')\n enum image/* image/jpeg image/png image/gif image/webp image/svg+xml application/pdf text/plain text/html text/css text/javascript text/csv application/json application/xml application/zip application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio/* audio/mpeg audio/wav audio/ogg video/* video/mp4 video/webm video/ogg\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n description Set HTML attribute.\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMinParser\n cue min\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextMaxParser\n cue max\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextStepParser\n cue step\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextWidthParser\n cue width\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextHeightParser\n cue height\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextRoleParser\n popularity 0.000217\n cue role\n description Set ARIA role attribute for accessibility.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTabindexParser\n popularity 0.000217\n cue tabindex\n description Set tabindex attribute for keyboard navigation.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextColspanParser\n popularity 0.000217\n cue colspan\n description Set colspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextRowspanParser\n popularity 0.000217\n cue rowspan\n description Set rowspan attribute for table cells.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextMethodParser\n popularity 0.000217\n cue method\n description Set form method attribute (GET/POST).\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlMethodAtom\naftertextActionParser\n popularity 0.000217\n cue action\n description Set form action URL.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextAutocompleteParser\n popularity 0.000217\n cue autocomplete\n description Set form autocomplete attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextMultipleParser\n popularity 0.000217\n cue multiple\n description Allow multiple selections or files.\n extends abstractAftertextAttributeParser\n single\naftertextSelectedParser\n popularity 0.000217\n cue selected\n description Set selected state for options.\n extends abstractAftertextAttributeParser\n single\naftertextCheckedParser\n popularity 0.000217\n cue checked\n description Set checked state for inputs.\n extends abstractAftertextAttributeParser\n single\naftertextRelParser\n popularity 0.000217\n cue rel\n description Set rel attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextEnctypeParser\n popularity 0.000217\n cue enctype\n description Set form enctype for file uploads.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextAcceptParser\n popularity 0.000217\n cue accept\n description Set accepted file types for file inputs.\n extends abstractAftertextAttributeParser\n catchAllAtomType mimeTypeAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
2 days ago
package.json
Changed around line 19
- "scroll-cli": "^168.5.0",
+ "scroll-cli": "^168.6.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n description Set HTML attribute.\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMinParser\n cue min\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextMaxParser\n cue max\n extends abstractAftertextAttributeParser\n atoms cueAtom numberAtom\naftertextWidthParser\n cue width\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextHeightParser\n cue height\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.5.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
2 days ago
Agents.js
Changed around line 117: class Claude extends AbstractAgent {
- class OpenAIAgent extends AbstractAgent {}
-
Changed around line 126: class DeepSeek extends AbstractAgent {
+ model = "deepseek-chat"
- model: "deepseek-chat"
+ model: this.model
Changed around line 140: class DeepSeek extends AbstractAgent {
- const AgentClasses = { claude: Claude, deepseek: DeepSeek, openai: OpenAIAgent }
+ class DeepSeekReasoner extends DeepSeek {
+ model = "deepseek-reasoner"
+ name = "DeepSeekReasoner"
+ }
Changed around line 164: class Agents {
- const agentConstructor = AgentClasses[name]
- this.agents[name] = new agentConstructor(apiKey, hubFolder)
+ const AgentClasses = { claude: [Claude], deepseek: [DeepSeek, DeepSeekReasoner] }
+ const agentConstructors = AgentClasses[name]
+ agentConstructors.forEach(con => {
+ const agent = new con(apiKey, hubFolder)
+ return (this.agents[agent.name] = agent)
+ })
public/promptSettings.scroll
Changed around line 27: css
+
Breck Yunits
Breck Yunits
2 days ago
ScrollHub.js
Changed around line 1940: scrollVersionLink`
- this.makeCert(hostname)
- throw new Error(`SSL certificate or key not found for ${hostname}. Attempting to make cert.`)
+
+ if (this.folderCache[hostname]) {
+ this.makeCert(hostname)
+ throw new Error(`SSL certificate or key not found for ${hostname}. Attempting to make cert.`)
+ } else {
+ throw new Error(`${hostname} requested but no matching folder found.`)
+ }
Breck Yunits
Breck Yunits
2 days ago
ScrollHub.js
Changed around line 1992: scrollVersionLink`
- const lowercaseHostname = hostname.toLowerCase()
-
- // First check if this is a valid folder/hostname we're serving
- if (!that.folderCache[lowercaseHostname]) console.error(`Rejected certificate request for unknown hostname: ${lowercaseHostname}`)
- return cb(new Error("Unknown hostname"))
-
- const sslOptions = that.loadCertAndKey(lowercaseHostname)
+ const sslOptions = that.loadCertAndKey(hostname.toLowerCase())
- console.error(`No cert found for ${lowercaseHostname}: ${err.message}`)
+ console.error(`No cert found for ${hostname}: ${err.message}`)
Breck Yunits
Breck Yunits
2 days ago
ScrollHub.js
Changed around line 1992: scrollVersionLink`
+ const lowercaseHostname = hostname.toLowerCase()
+
+ // First check if this is a valid folder/hostname we're serving
+ if (!that.folderCache[lowercaseHostname]) console.error(`Rejected certificate request for unknown hostname: ${lowercaseHostname}`)
+ return cb(new Error("Unknown hostname"))
+
- const sslOptions = that.loadCertAndKey(hostname.toLowerCase())
+ const sslOptions = that.loadCertAndKey(lowercaseHostname)
- console.error(`No cert found for ${hostname}: ${err.message}`)
+ console.error(`No cert found for ${lowercaseHostname}: ${err.message}`)
Breck Yunits
Breck Yunits
2 days ago
public/templates/frame_template/demo.scroll
Changed around line 1
+ buildHtml
+ style.css
+ jframe.parsers
+ title Simple Farcaster Frames v2 Demo
+ jframeMetaTags
+
+ div
+ class container
+ h1 Farcaster Frames v2 Demo
+ div Waiting for frame to load...
+ id status
+ class status
+ div
+ button Show Context
+ id contextBtn
+ onclick app.getContext()
+ disabled
+ button Open URL
+ id urlBtn
+ onclick app.openUrl()
+ disabled
+ button Close Frame
+ id closeBtn
+ onclick app.closeFrame()
+ disabled
+ button Test Primary Button
+ id testBtn
+ onclick app.testPrimaryButton()
+ disabled
+ div
+ id contextData
+
+ https://jframe.breckyunits.com/jframe.js
+ frame.js
public/templates/frame_template/frame.js
Changed around line 1
+ class App {
+ constructor() {
+ this.statusDiv = document.getElementById("status")
+ this.contextDiv = document.getElementById("contextData")
+ this.buttons = ["contextBtn", "urlBtn", "closeBtn", "testBtn"].map(id => document.getElementById(id))
+ this.isSDKLoaded = false
+ }
+
+ setStatus(message) {
+ if (this.statusDiv) {
+ this.statusDiv.innerText = message
+ }
+ }
+
+ enableButtons() {
+ this.buttons.forEach(btn => {
+ if (btn) btn.disabled = false
+ })
+ }
+
+ get sdk() {
+ return jframe.sdk
+ }
+
+ async start() {
+ try {
+ await this.sdk.actions.ready()
+ this.isSDKLoaded = true
+ this.setStatus("Frame is ready!")
+ this.enableButtons()
+
+ this.sdk.on("primaryButtonClicked", () => {
+ this.setStatus("Primary button was clicked!")
+ })
+ } catch (error) {
+ this.setStatus(`Error initializing frame: ${error.message}`)
+ }
+ }
+
+ async getContext() {
+ if (!this.isSDKLoaded) return
+ try {
+ const context = await this.sdk.context
+ if (this.contextDiv) {
+ this.contextDiv.innerHTML = `
+

Frame Context:

+
${JSON.stringify(context, null, 2)}
+ `
+ }
+ } catch (error) {
+ this.setStatus(`Error getting context: ${error.message}`)
+ }
+ }
+
+ openUrl() {
+ if (!this.isSDKLoaded) return
+ try {
+ this.sdk.actions.openUrl("https://www.farcaster.xyz")
+ this.setStatus("Opening URL...")
+ } catch (error) {
+ this.setStatus(`Error opening URL: ${error.message}`)
+ }
+ }
+
+ closeFrame() {
+ if (!this.isSDKLoaded) return
+ try {
+ this.sdk.actions.close()
+ this.setStatus("Closing frame...")
+ } catch (error) {
+ this.setStatus(`Error closing frame: ${error.message}`)
+ }
+ }
+
+ testPrimaryButton() {
+ if (!this.isSDKLoaded) return
+ try {
+ this.sdk.actions.setPrimaryButton({
+ text: "Click Me!",
+ loading: false,
+ disabled: false
+ })
+ this.setStatus("Primary button set - try clicking it!")
+ } catch (error) {
+ this.setStatus(`Error setting primary button: ${error.message}`)
+ }
+ }
+ }
+
+ document.addEventListener("DOMContentLoaded", () => {
+ window.app = new App()
+ window.app.start()
+ })
public/templates/frame_template/style.css
Changed around line 1
+ body {
+ font-family:
+ system-ui,
+ -apple-system,
+ sans-serif;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 20px;
+ background: #f5f5f5;
+ }
+ .container {
+ background: white;
+ padding: 20px;
+ border-radius: 12px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+ button {
+ background: #0070f3;
+ color: white;
+ border: none;
+ padding: 10px 20px;
+ border-radius: 6px;
+ cursor: pointer;
+ margin: 5px;
+ }
+ button:hover {
+ background: #0051b3;
+ }
+ button:disabled {
+ background: #ccc;
+ cursor: not-allowed;
+ }
+ .status {
+ margin: 20px 0;
+ padding: 10px;
+ border-radius: 6px;
+ background: #e2e8f0;
+ }
+ pre {
+ background: #f8f9fa;
+ padding: 10px;
+ border-radius: 6px;
+ overflow-x: auto;
+ }
Breck Yunits
Breck Yunits
2 days ago
Add frames microlang
.gitignore
Changed around line 18: list.scroll
- certs/
- blank_template
+ certs/
ScrollHub.js
Changed around line 1703: If you'd like to create this folder, visit our main site to get started.
- const templatesFolder = path.join(__dirname, "templates")
+ const templatesFolder = path.join(__dirname, "public", "templates")
microlangs/frames.parsers
Changed around line 1
+ // Frame Parser and its components
+ frameParser
+ description Create a Farcaster Frame
+ inScope frameVersionParser frameImageParser frameButtonParser frameHeaderParser frameSplashParser frameNameParser frameUrlParser frameWebhookParser frameBackgroundParser frameButtonTitleParser frameIconParser frameNotificationsParser frameHeightParser frameWidthParser frameStyleParser
+ extends abstractScrollParser
+ cue frame
+ example
+ frame
+ version next
+ name Example Frame
+ image https://example.com/image.png
+ button Click me!
+ url https://example.com/frame
+ splash https://example.com/splash.png
+ background #eeeee4
+ notifications true
+ webhook https://example.com/webhook
+ javascript
+ buildHtml() {
+ // Convert frame settings into required meta tags
+ const frameEmbed = {
+ version: this.get("version") || "next",
+ imageUrl: this.get("image"),
+ button: {
+ title: this.get("button") || this.get("buttonTitle"),
+ action: {
+ type: "launch_frame",
+ name: this.get("name"),
+ url: this.get("url"),
+ splashImageUrl: this.get("splash"),
+ splashBackgroundColor: this.get("background")
+ }
+ }
+ }
+ return `
+
+
+
+
+ `
+ }
+
+ frameVersionParser
+ extends abstractScrollParser
+ cue version
+ atoms cueAtom stringAtom
+ description Set the frame version
+
+ frameNameParser
+ extends abstractScrollParser
+ cue name
+ atoms cueAtom stringAtom
+ description Set the frame name
+
+ frameImageParser
+ extends abstractScrollParser
+ cue image
+ atoms cueAtom urlAtom
+ description Set the frame image URL
+
+ frameButtonParser
+ extends abstractScrollParser
+ cue button
+ atoms cueAtom stringAtom
+ description Set the button text
+
+ frameUrlParser
+ extends abstractScrollParser
+ cue url
+ atoms cueAtom urlAtom
+ description Set the frame action URL
+
+ frameHeaderParser
+ extends abstractScrollParser
+ cue header
+ atoms cueAtom stringAtom
+ description Set the frame header text
+
+ frameSplashParser
+ extends abstractScrollParser
+ cue splash
+ atoms cueAtom urlAtom
+ description Set the splash screen image URL
+
+ frameBackgroundParser
+ extends abstractScrollParser
+ cue background
+ atoms cueAtom colorAtom
+ description Set the splash background color
+
+ frameButtonTitleParser
+ extends abstractScrollParser
+ cue buttonTitle
+ atoms cueAtom stringAtom
+ description Set the button title
+
+ frameIconParser
+ extends abstractScrollParser
+ cue icon
+ atoms cueAtom urlAtom
+ description Set the frame icon URL
+
+ frameNotificationsParser
+ extends abstractScrollParser
+ cue notifications
+ atoms cueAtom booleanAtom
+ description Enable/disable frame notifications
+
+ frameWebhookParser
+ extends abstractScrollParser
+ cue webhook
+ atoms cueAtom urlAtom
+ description Set the webhook URL for notifications
+
+ frameHeightParser
+ extends abstractScrollParser
+ cue height
+ atoms cueAtom integerAtom
+ description Set frame height in pixels
+
+ frameWidthParser
+ extends abstractScrollParser
+ cue width
+ atoms cueAtom integerAtom
+ description Set frame width in pixels
+
+ frameStyleParser
+ extends abstractScrollParser
+ cue style
+ atoms cueAtom cssAnyAtom
+ description Add custom CSS styles
public/templates/blank_template/.gitignore
public/templates/blank_template/index.scroll
public/templates/frame_template/.well-known/farcaster.json
Changed around line 1
+ {
+ "accountAssociation": {
+ "header": "eyJmaWQiOjM2MjEsInR5cGUiOiJjdXN0b2R5Iiwia2V5IjoiMHgyY2Q4NWEwOTMyNjFmNTkyNzA4MDRBNkVBNjk3Q2VBNENlQkVjYWZFIn0",
+ "payload": "eyJkb21haW4iOiJmcmFtZXMtdjItc3dhcC1kZW1vLnZlcmNlbC5hcHAifQ",
+ "signature": "MHgxMzE0NDBjODMyMWRkM2UzNmQ3OWFiNDYxYmNiZThiOTM0NGNkOGZkNmVhMmVlNmY3YTY4NWJiNjMzMTYyNGNjNTczNjUyNTlhNzE5MTJkZDM4NWVmZmM5MWMwZmY1ZWVlMzYwNGUzYWNiZTI3MTQzYzIwYTRjMDBlNjgwZjBmNzFj"
+ },
+ "frame": {
+ "version": "0.0.0",
+ "name": "FrameHub",
+ "iconUrl": "https://hub.scroll.pub/templates/frame_template/splash.png",
+ "splashImageUrl": "https://hub.scroll.pub/templates/frame_template/splash.png",
+ "splashBackgroundColor": "purple",
+ "homeUrl": "https://hub.scroll.pub/framehub.html"
+ }
+ }
public/templates/frame_template/splash.png
Breck Yunits
Breck Yunits
2 days ago
package.json
Changed around line 19
- "scroll-cli": "^168.4.0",
+ "scroll-cli": "^168.5.0",
Breck Yunits
Breck Yunits
2 days ago
public/framehub.scroll
Changed around line 71: Built by Breck
+ br 2
+
+ center
+ Frame Developer Tools
+ class greyText
+ https://warpcast.com/~/developers/frames
+
Breck Yunits
Breck Yunits
3 days ago
Agents.js
Changed around line 68: ${domainExpression}`
- while (this.existingFolders.[finalDomain]) {
+ while (this.existingFolders[finalDomain]) {
Breck Yunits
Breck Yunits
3 days ago
package.json
Changed around line 19
- "scroll-cli": "^168.3.0",
+ "scroll-cli": "^168.4.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nhtmlMetaTagParser\n description HTML meta tag.\n extends abstractHtmlElementParser\n cue meta\n example\n meta\n name description\n content A great page about meta tags\n javascript\n tag = \"meta\"\n isSelfClosing = true\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n get tag() {return \"p\"}\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nabstractHtmlElementParser\n description HTML tag.\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\n get tag() { return this.cue}\nhtmlSectionParser\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\nhtmlHeaderParser\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\nhtmlFooterParser\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\nhtmlAsideParser\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\nhtmlArticleParser\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\nhtmlMainParser\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\nhtmlNavParser\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\nhtmlPreParser\n extends abstractHtmlElementParser\n cue pre\n example\n pre\nhtmlUlParser\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\nhtmlOlParser\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\nhtmlLiParser\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\nhtmlImgParser\n extends abstractHtmlElementParser\n cue img\n boolean isSelfClosing true\n example\n img foo.png\nhtmlAParser\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\nhtmlFormParser\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\nhtmlInputParser\n extends abstractHtmlElementParser\n cue input\n boolean isSelfClosing true\n example\n input\n type text\n placeholder Enter your name\nhtmlSelectParser\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\nhtmlOptionParser\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\nhtmlTextareaParser\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\nhtmlButtonParser\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\nhtmlLabelParser\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\nhtmlSpanParser\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\nhtmlCanvasParser\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\nhtmlIframeParser\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\nh1LiteralParser\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\nh2LiteralParser\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\nh3LiteralParser\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\nh4LiteralParser\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\nhtmlKbdParser\n extends abstractHtmlElementParser\n cue kbd\n example\n main\n kbd Ctrl+→\nhtmlMetaTagParser\n extends abstractHtmlElementParser\n cue meta\n boolean isSelfClosing true\n example\n meta\n name description\n content A great page about meta tags\nhtmlDivParser\n extends abstractHtmlElementParser\n cue div\n example\n div\n # Hello world\n div\n # Deeper\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
3 days ago
Agents.js
Changed around line 5: const OpenAI = require("openai")
- constructor(userPrompt, existingFolderNames, agent, whatKind, domainSuffix) {
+ constructor(userPrompt, existingFolders, agent, whatKind, domainSuffix) {
- this.existingNames = existingFolderNames
+ this.existingFolders = existingFolders
Changed around line 68: ${domainExpression}`
- while (this.existingNames.includes(finalDomain)) {
+ while (this.existingFolders.[finalDomain]) {
Changed around line 170: class Agents {
- async createFolderNameAndFilesFromPrompt(userPrompt, existingNames, agentName, promptTemplate, domainSuffix) {
+ async createFolderNameAndFilesFromPrompt(userPrompt, existingFolders, agentName, promptTemplate, domainSuffix) {
- const prompt = new FolderPrompt(userPrompt, existingNames, agent, promptTemplate, domainSuffix)
+ const prompt = new FolderPrompt(userPrompt, existingFolders, agent, promptTemplate, domainSuffix)
- async createMultipleFoldersFromPrompt(userPrompt, existingNames) {
+ async createMultipleFoldersFromPrompt(userPrompt, existingFolders) {
- const prompt = new SimpleCreationPrompt(userPrompt, existingNames)
+ const prompt = new SimpleCreationPrompt(userPrompt, existingFolders)
ScrollHub.js
Changed around line 736: If you'd like to create this folder, visit our main site to get started.
- // Get existing names for domain uniqueness check
- const existingNames = Object.keys(this.folderCache)
-
- const response = await agents.createFolderNameAndFilesFromPrompt(prompt, existingNames, agent, template, domainSuffix)
+ const response = await agents.createFolderNameAndFilesFromPrompt(prompt, this.folderCache, agent, template, domainSuffix)
Breck Yunits
Breck Yunits
5 days ago
public/framehub.png
public/framehub.scroll
Changed around line 6: openGraphImage framehub.png
+
Breck Yunits
Breck Yunits
5 days ago
ScrollHub.js
Changed around line 732: If you'd like to create this folder, visit our main site to get started.
+ const welcomeMessage = req.body.welcomeMessage || "scrollhub"
Changed around line 755: If you'd like to create this folder, visit our main site to get started.
- res.redirect(`/edit.html?folderName=${folderName}&command=showWelcomeMessageCommand`)
+ res.redirect(`/edit.html?folderName=${folderName}&command=showWelcomeMessageCommand&welcomeMessage=${welcomeMessage}`)
public/ai.scroll
Changed around line 2: indexTop.scroll
+ replace WELCOME_MESSAGE scrollhub
+ replace SOCIAL_LINK X
public/createFromPrompt.scroll
Changed around line 3
+
Changed around line 14: inlineJs create.js
-

While you wait, say hi on X

+

While you wait, say hi on SOCIAL_LINK

public/framehub.scroll
Changed around line 54: css
+ replace WELCOME_MESSAGE framehub
+ replace SOCIAL_LINK Warpcast
public/scrollHubEditor.js
Changed around line 688: nokey1 showWelcomeMessageCommand Help`
- const content = `container 600px
+ let content = `container 600px
Changed around line 713: I'd love to hear your requests and feedback! Contact me on X, GitHub, or email.
- Breck`
+ if (new URL(window.location).searchParams.get("welcomeMessage") === "framehub") {
+ content = `container 600px
+
+ # Welcome to FrameHub!
+
+ center
+ ${this.folderName} is live!
+
+
+ center
+ Visit ${this.folderName}
+ link ${this.permalink}
+ target preview
+ class newSiteLink
+
+ I'd love to hear your requests and feedback! Find me on Warpcast.
+ https://warpcast.com/breck Find me on Warpcast
+
+ -Breck`
+ }
Breck Yunits
Breck Yunits
5 days ago
prompts/frame.scroll
Changed around line 114: metaTags
- meta
- name fc:frame
- content {\"version\":\"vNext\",\"image\":{\"src\":\"FRAME_IMAGE_URL\",\"aspectRatio\":\"1.91:1\"},\"buttons\":[{\"label\":\"SHOOT! 🏀\",\"action\":\"post\"}]}"
+
+ replaceNodejs
+ module.exports = {JSONCONTENTS: JSON.stringify(JSON.parse(require("fs").readFileSync("meta.json")))}
+
+
+
Changed around line 261: document.addEventListener('DOMContentLoaded', initializeFrame);
- "postUrl": "https://DOMAIN_EXPRESSION/api/frame"
+ "postUrl": "https://DOMAIN_EXPRESSION/api"
+ }
+
+ ---meta.json---
+ {
+ "version": "next",
+ "imageUrl": "FRAME_IMAGE_URL",
+ "button": {
+ "title": "Launch (something)",
+ "action": {
+ "type": "launch_frame",
+ "name": "(Frame Name)",
+ "url": "FRAME_URL",
+ "splashImageUrl": "FRAME_SPLASH_IMAGE",
+ "splashBackgroundColor": "FRAME_BG_COLOR"
+ }
+ }
+ ---api.js---
Breck Yunits
Breck Yunits
5 days ago
ScrollHub.js
Changed around line 1616: If you'd like to create this folder, visit our main site to get started.
- const prettierExtensions = [".js", ".html", ".css"]
+ const prettierExtensions = [".js", ".html", ".css", ".json", ".htm", ".mjs"]
Breck Yunits
Breck Yunits
5 days ago
package.json
Changed around line 19
- "scroll-cli": "^168.2.0",
+ "scroll-cli": "^168.3.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nhtmlMetaTagParser\n description HTML meta tag.\n extends abstractHtmlElementParser\n cue meta\n example\n meta\n name description\n content A great page about meta tags\n javascript\n tag = \"meta\"\n isSelfClosing = true\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.1.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nhtmlMetaTagParser\n description HTML meta tag.\n extends abstractHtmlElementParser\n cue meta\n example\n meta\n name description\n content A great page about meta tags\n javascript\n tag = \"meta\"\n isSelfClosing = true\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextDataParser\n popularity 0.000217\n pattern ^data\\-\n description Set HTML data- attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom\n catchAllAtomType htmlAnyAtom\n example\n div My score\n data-score 100\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
5 days ago
frame building for wc hackathon
prompts/frame.scroll
Changed around line 1
+ You are an expert web and Warpcast/Farcaster developer. Create a WarpCast v2 Frame based on this request: "USER_PROMPT"
+
+ Requirements:
+ - Use Farcaster Frames v2
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ The Frames v2 docs are here: https://framesv2.com/
+ The Frames v2 dev tools are here: https://warpcast.com/~/developers/frames
+
+ As a refresher, for doing the html body, Scroll is a whitespace based language that uses a single indented space to mark a line (aka particle) as a subparticle of a parent line.
+
+ For example:
+
+ header
+ class hero
+ nav
+ div Scroll
+ class logo
+ div
+ class nav-links
+ a Features
+ href #features
+ a Examples
+ href #examples
+ a Edit
+ href edit.html
+ a GitHub
+ class cta-button
+ href https://github.com/breck7/scroll
+ div
+ class hero-content
+ h1 Write Better with Scroll
+ p The extendible markup language that makes source beautiful and compiles to anything
+ a Get Started
+ class primary-button
+ href https://hub.scroll.pub/
+ main
+ section
+ id features
+ class features
+ h2 Why Scroll?
+ div
+ class feature-grid
+ div
+ class feature-card
+ div ⚡
+ class feature-icon
+ h3 Simple Syntax
+ p Like Markdown, but more powerful. No parentheses needed.
+ div
+ class feature-card
+ div 🧩
+ class feature-icon
+ h3 Extendible
+ p Build your own custom parsers.
+ div
+ class feature-card
+ div 🎨
+ class feature-icon
+ h3 Beautiful Output
+ p Create stunning documents with minimal effort.
+ div
+ class feature-card
+ div 🚀
+ class feature-icon
+ h3 Fast & Light
+ p Built on the efficient PPS Stack.
+ section
+ id examples
+ class code-demo
+ h2 See It In Action
+ div
+ class code-container
+ pre
+ class code-example
+ div
+ class code-output
+ footer
+ div
+ class footer-content
+ div
+ class footer-links
+ a Documentation
+ href https://scroll.pub/tutorial.html
+ a Community
+ href https://www.reddit.com/r/WorldWideScroll/
+ a Blog
+ href https://scroll.pub/blog
+ p Started by Breck Yunits. Evolved by a community.
+ https://twitter.com/breckyunits Breck Yunits
+ https://github.com/breck7/scroll/graphs/contributors community
+
+
+ DOMAIN_PROMPT
+ ---index.scroll---
+ buildHtml
+ baseUrl https://DOMAIN_EXPRESSION
+ import head.scroll
+ title FRAME_TITLE
+ description FRAME_DESCRIPTION
+ style.css
+ frame.scroll
+ script.js
+
+ ---head.scroll---
+ metaTags
+ meta
+ name viewport
+ content width=device-width, initial-scale=1.0
+ meta
+ name fc:frame
+ content {\"version\":\"vNext\",\"image\":{\"src\":\"FRAME_IMAGE_URL\",\"aspectRatio\":\"1.91:1\"},\"buttons\":[{\"label\":\"SHOOT! 🏀\",\"action\":\"post\"}]}"
+ meta
+ name fc:frame:image
+ content FRAME_IMAGE_URL
+ meta
+ name fc:frame:post_url
+ content https://DOMAIN_EXPRESSION/api/frame
+
+ ---frame.scroll---
+ main
+ class frameContainer
+ div
+ class frameContent
+ h1 FRAME_TITLE
+ class frameTitle
+ div
+ class frameActionArea
+ button Start
+ class frameButton
+ data-action post
+
+ ---style.css---
+ :root {
+ --frame-bg: #1c1c1c;
+ --frame-text: #ffffff;
+ --frame-accent: #5277FF;
+ --frame-radius: 12px;
+ }
+
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ background: var(--frame-bg);
+ color: var(--frame-text);
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .frameContainer {
+ width: 100%;
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 20px;
+ }
+
+ .frameContent {
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: var(--frame-radius);
+ padding: 24px;
+ backdrop-filter: blur(10px);
+ }
+
+ .frameTitle {
+ font-size: 24px;
+ font-weight: 700;
+ margin-bottom: 16px;
+ text-align: center;
+ }
+
+ .frameActionArea {
+ display: flex;
+ justify-content: center;
+ margin-top: 24px;
+ }
+
+ .frameButton {
+ background: var(--frame-accent);
+ color: var(--frame-text);
+ border: none;
+ padding: 12px 24px;
+ border-radius: var(--frame-radius);
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .frameButton:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(82, 119, 255, 0.25);
+ }
+
+ @media (max-width: 480px) {
+ .frameContainer {
+ padding: 16px;
+ }
+
+ .frameTitle {
+ font-size: 20px;
+ }
+ }
+
+ ---script.js---
+ // Initialize Frame SDK
+ const initializeFrame = async () => {
+ try {
+ // Initialize Frame context
+ const frameContext = await window.frames.getContext();
+ console.log('Frame context:', frameContext);
+
+ // Handle button clicks
+ document.querySelector('.frameButton').addEventListener('click', async () => {
+ try {
+ await window.frames.postMessage({
+ action: 'post'
+ });
+ } catch (error) {
+ console.error('Frame action error:', error);
+ }
+ });
+
+ // Signal frame is ready
+ await window.frames.ready();
+ } catch (error) {
+ console.error('Frame initialization error:', error);
+ }
+ };
+
+ // Start Frame when DOM is loaded
+ document.addEventListener('DOMContentLoaded', initializeFrame);
+
+ ---manifest.json---
+ {
+ "version": "vNext",
+ "name": "FRAME_TITLE",
+ "description": "FRAME_DESCRIPTION",
+ "image": {
+ "src": "FRAME_IMAGE_URL",
+ "aspectRatio": "1.91:1"
+ },
+ "buttons": [
+ {
+ "label": "START",
+ "action": "post"
+ }
+ ],
+ "postUrl": "https://DOMAIN_EXPRESSION/api/frame"
+ }
+
+ ---api/frame.js---
+ // Frame API handler
+ export default async function handler(req, res) {
+ if (req.method !== 'POST') {
+ return res.status(405).json({ error: 'Method not allowed' });
+ }
+
+ try {
+ // Handle Frame interaction
+ const frameData = req.body;
+
+ // Process frame action here
+
+ // Return next frame state
+ return res.status(200).json({
+ version: 'vNext',
+ image: {
+ src: 'FRAME_IMAGE_URL',
+ aspectRatio: '1.91:1'
+ },
+ buttons: [
+ {
+ label: 'CONTINUE',
+ action: 'post'
+ }
+ ]
+ });
+ } catch (error) {
+ console.error('Frame API error:', error);
+ return res.status(500).json({ error: 'Internal server error' });
+ }
+ }
+
+ ---end---
public/framehub.scroll
Changed around line 1
+ buildHtml
+ title FrameHub: the fastest way to launch frames
+
+ baseUrl http://framehub.pro/
+ openGraphImage framehub.png
+ editButton https://github.com/breck7/framehub
+ editBaseUrl https://github.com/breck7/framehub/blob/main
+
+
+ metaTags
+ inlineCss .gazette.css .scroll.css scrollHubStyle.css
+ css html[data-theme=dark], html[data-theme=dark] * { color-scheme: dark; }
+ css .thin {font-weight: 100;}
+
+ # *Frame*Hub
+ style font-size: 320%; font-weight:200;
+ ## The _fastest_ way to launch frames
+
+ br 2
+
+ css
+ :root {
+ --primary-color: #1e1e2e;
+ --secondary-color: #353348;
+ --accent-color: #6b5bc4;
+ --scrollColorLink: white;
+ --scrollPrimaryRgb: 104,77,179;
+ }
+ * {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ z-index: 1;
+ }
+ body {
+ min-height: 100vh;
+ background: radial-gradient(circle at center, var(--secondary-color), var(--primary-color));
+ font-family: system-ui, -apple-system, sans-serif;
+ color: white;
+ position: relative;
+ }
+ #nodesCanvas {
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ }
+
+ // nodes.js
+
+
+ replace AGENT Claude
+ replace TEMPLATE frame
+ replace TLD framehub.pro
+ createFromPrompt.scroll
+
+ //
+ br 2
+ center
+ foldersPublished.html
+
+ br 2
+
+ center
+ Built by Breck
+ class greyText
+ https://warpcast.com/breck
+
+ //
+ https://github.com/breck7/week-1
+ https://github.com/weeklyhackathon/week-1
+ https://warpcast.com/~/developers/frames
public/promptSettings.scroll
Changed around line 22: css
+ // Todo: generate these options dynamically on server
Changed around line 31: css
+
Breck Yunits
Breck Yunits
5 days ago
package.json
Changed around line 19
- "scroll-cli": "^168.1.0",
+ "scroll-cli": "^168.2.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nhtmlMetaTagParser\n description HTML meta tag.\n extends abstractHtmlElementParser\n cue meta\n example\n meta\n name description\n content A great page about meta tags\n javascript\n tag = \"meta\"\n isSelfClosing = true\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextContentParser\n popularity 0.000217\n cue content\n description Set HTML content attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.1.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
5 days ago
public/ai.scroll
Changed around line 3: replace AGENT Claude
+ promptSettings.scroll
public/createFromPrompt.scroll
Changed around line 68: script
-
-
- promptSettings.scroll
public/nodes.js
Changed around line 4: class Node {
- this.radius = Math.min(folderData.revisions, 1)
+ //this.radius = Math.min(folderData.revisions, 1)
+ this.radius = Math.max(Math.log2(folderData.revisions), 3)
public/nodes.scroll
Changed around line 1
- //
- -primary-color: #1a2b4d;
Changed around line 26: css
+ //
Breck Yunits
Breck Yunits
5 days ago
Create from files method
ScrollHub.js
Changed around line 1018: If you'd like to create this folder, visit our main site to get started.
+ app.post("/createFolderFromFiles.htm", checkWritePermissions, async (req, res) => {
+ if (!req.files || !req.files["files[]"]) return res.status(400).send("No files were uploaded.")
+
+ try {
+ // Generate a unique folder name
+ const folderName = await this.findAvailableFolderName()
+ const folderPath = path.join(this.rootFolder, folderName)
+
+ // Create the folder
+ await fsp.mkdir(folderPath, { recursive: true })
+
+ // Handle multiple files
+ const uploadedFiles = Array.isArray(req.files["files[]"]) ? req.files["files[]"] : [req.files["files[]"]]
+
+ // Process each file
+ for (const file of uploadedFiles) {
+ const filePath = path.join(folderPath, file.name)
+
+ // Create necessary subdirectories
+ await fsp.mkdir(path.dirname(filePath), { recursive: true })
+
+ // Move the file to its destination
+ await file.mv(filePath)
+ }
+
+ // Initialize git repository
+ await execAsync("git init", { cwd: folderPath })
+ await execAsync("git add .", { cwd: folderPath })
+ await execAsync('git commit -m "Initial commit from uploaded files"', { cwd: folderPath })
+
+ // Add to story and update caches
+ this.addStory(req, `created ${folderName} from uploaded files`)
+ await this.updateFolderAndBuildList(folderName)
+
+ // Build the folder
+ await this.buildFolder(folderName)
+
+ // Send the folder name back
+ res.send(folderName)
+ } catch (error) {
+ console.error(`Error creating folder from files:`, error)
+ res.status(500).send(`An error occurred while creating folder from files: ${error.toString().replace(/
+ }
+ })
+
Changed around line 1311: If you'd like to create this folder, visit our main site to get started.
+ async findAvailableFolderName(prefix = "files") {
+ const { folderCache } = this
+ let counter = 1
+ let folderName
+ do {
+ folderName = `${prefix}${counter}`
+ counter++
+ } while (folderCache[folderName])
+ return folderName
+ }
+
public/create.js
Changed around line 39: class CreateFromZipper {
- showSpinner(message, style) {
+ showSpinner(message, style = "") {
+
- // New method to handle multiple file uploads
- uploadFiles(files) {
+ async uploadFiles(files) {
- const uploadPromises = Array.from(files).map(file => this.uploadFile(file))
-
- Promise.all(uploadPromises)
- .then(() => {
- console.log("All files uploaded successfully")
- this.hideSpinner()
- })
- .catch(error => {
- console.error("Error uploading files:", error)
- // todo: show error to user
- alert("Error uploading files:" + error)
- })
+
+ try {
+ // Check if it's a single zip file
+ if (files.length === 1 && files[0].name.toLowerCase().endsWith(".zip")) {
+ await this.uploadZipFile(files[0])
+ } else {
+ // Handle multiple files or single non-zip file
+ await this.uploadMultipleFiles(files)
+ }
+
+ this.hideSpinner()
+ } catch (error) {
+ console.error("Error uploading files:", error)
+ alert("Error uploading files: " + error)
+ this.hideSpinner()
+ }
- // Modified uploadFile method to return a Promise
- async uploadFile(file) {
+ async uploadZipFile(file) {
- try {
- const response = await fetch("/createFolderFromZip.htm", {
- method: "POST",
- body: formData
- })
-
- if (!response.ok) {
- const errorText = await response.text()
- throw new Error(errorText || "Network response was not ok")
- }
+ const response = await fetch("/createFolderFromZip.htm", {
+ method: "POST",
+ body: formData
+ })
- const data = await response.text()
- window.location = `/edit.html?folderName=${data}&command=showWelcomeMessageCommand`
- } catch (error) {
- console.error("Error uploading file:", error.message)
- throw error // Re-throw the error if you want calling code to handle it
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || "Network response was not ok")
+
+ const data = await response.text()
+ window.location = `/edit.html?folderName=${data}&command=showWelcomeMessageCommand`
+ }
+
+ async uploadMultipleFiles(files) {
+ const formData = new FormData()
+
+ // Append all files to the form data
+ Array.from(files).forEach(file => {
+ // Use the full path if available (for folders), otherwise just the file name
+ const filePath = file.webkitRelativePath || file.name
+ formData.append("files[]", file, filePath)
+ })
+
+ const response = await fetch("/createFolderFromFiles.htm", {
+ method: "POST",
+ body: formData
+ })
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || "Network response was not ok")
+ }
+
+ const data = await response.text()
+ window.location = `/edit.html?folderName=${data}&command=showWelcomeMessageCommand`
Breck Yunits
Breck Yunits
5 days ago
Serve mjs files correctly
ScriptRunner.js
Changed around line 4: const path = require("path")
- mjs: "node",
+ nodejs: "node",
package.json
Changed around line 19
- "scroll-cli": "^168.0.0",
+ "scroll-cli": "^168.1.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n attr = \"\"\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickModuleScriptParser\n description Make a Javascript module tag.\n extends quickScriptParser\n example\n script.mjs\n pattern ^[^\\s]+\\.(mjs)$\n javascript\n attr = 'type=\"module\"'\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"168.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
6 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.7.0",
+ "scroll-cli": "^168.0.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"class\"))\n return this.get(\"class\")\n const classLine = this.getParticle(\"addClass\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope addClassMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextOnsubmitParser\n popularity 0.000217\n cue onsubmit\n description Set HTML onsubmit attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextClassParser\n popularity 0.000217\n cue class\n description Set HTML class attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType classNameAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\naddClassMarkupParser\n popularity 0.000772\n description Add a custom class to parent element. Provide query to add span matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue addClass\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends addClassMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
6 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.6.0",
+ "scroll-cli": "^167.7.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.5.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlAsideParser\n description HTML aside tag.\n extends abstractHtmlElementParser\n cue aside\n example\n aside\n h1 Some notes\n javascript\n tag = \"aside\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.7.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
6 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.5.0",
+ "scroll-cli": "^167.6.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nhtmlCanvasParser\n description HTML canvas tag.\n extends abstractHtmlElementParser\n cue canvas\n example\n canvas\n javascript\n tag = \"canvas\"\nhtmlIframeParser\n description HTML iframe tag.\n extends abstractHtmlElementParser\n cue iframe\n example\n iframe\n javascript\n tag = \"iframe\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.5.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
7 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.4.0",
+ "scroll-cli": "^167.5.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlArticleParser\n description HTML article tag.\n extends abstractHtmlElementParser\n cue article\n example\n article\n h1 My article\n javascript\n tag = \"article\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
7 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.3.0",
+ "scroll-cli": "^167.4.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n description Base parser all measures extend.\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description Contains a single word.\n atoms measureNameAtom atomAtom\n example\n nicknameParser\n extends abstractAtomMeasureParser\n id Breck\n nickname breck\n extends abstractMeasureParser\nabstractEmailMeasureParser\n description Email address.\n example\n emailParser\n extends abstractEmailMeasureParser\n id Breck\n email breck7@gmail.com\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n description A single url.\n example\n homepageParser\n extends abstractUrlMeasureParser\n id Breck\n homepage https://breckyunits.com\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n description General text data with no specific format.\n catchAllAtomType stringAtom\n example\n titleParser\n extends abstractStringMeasureParser\n id Breck\n title I build languages for scientists of all ages\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n example\n idParser\n extends abstractIdParser\n id breck\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractIdMeasureParser\n description Alias for abstractIdParser.\n extends abstractIdParser\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n example\n bioParser\n extends abstractTextareaMeasureParser\n id Breck\n bio\n I build languages for scientists of all ages\n description Long-form text content with preserved line breaks.\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n description Base number type.\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractNumberMeasureParser\n description Alias to abstractNumericMeasureParser.\n extends abstractNumericMeasureParser\nabstractIntegerMeasureParser\n description An integer.\n example\n ageParser\n extends abstractIntegerMeasureParser\n id Breck\n age 40\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseInt(content)\n }\nabstractIntMeasureParser\n description Alias to abstractIntegerMeasureParser.\n extends abstractIntegerMeasureParser\nabstractFloatMeasureParser\n description A float.\n example\n temperatureParser\n extends abstractFloatMeasureParser\n id Breck\n temperature 31.8\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n description A percentage.\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n example\n ownershipParser\n extends abstractPercentageMeasureParser\n id Breck\n ownership 31.8\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n description A single enum.\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\n example\n favoriteHtmlTagParser\n extends abstractEnumMeasureParser\n atoms measureNameAtom htmlTagAtom\n id Breck\n favoriteHtmlTag 2020\nabstractBooleanMeasureParser\n description A single boolean.\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n example\n hasBillOfRightsParser\n extends abstractBooleanMeasureParser\n id USA\n hasBillOfRights true\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nabstractDateMeasureParser\n description Year/month/day in ISO 8601, US, European formats.\n atoms measureNameAtom dateAtom\n extends abstractMeasureParser\n string typeForWebForms date\n javascript\n get measureValue() {\n const {content} = this\n if (!content) return \"\"\n const {dayjs} = this.root\n try {\n // First try parsing with dayjs\n const parsed = dayjs(content)\n if (parsed.isValid())\n return parsed.format(\"YYYY-MM-DD\")\n // Try parsing other common formats\n const formats = [\n \"MM/DD/YYYY\",\n \"DD/MM/YYYY\", \n \"YYYY/MM/DD\",\n \"MM-DD-YYYY\",\n \"DD-MM-YYYY\",\n \"YYYY-MM-DD\",\n \"DD.MM.YYYY\",\n \"YYYY.MM.DD\"\n ]\n for (const format of formats) {\n const attempt = dayjs(content, format)\n if (attempt.isValid())\n return attempt.format(\"YYYY-MM-DD\")\n }\n } catch (err) {\n console.error(err)\n return \"\"\n }\n return \"\"\n }\n get valueAsTimestamp() {\n const {measureValue} = this\n return measureValue ? this.root.dayjs(measureValue).unix() : \"\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedComparisonValue = this.getAtom(3)\n const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedComparisonValue === typedAtom\n else if (operator === \"!=\") return typedComparisonValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)\n else if (operator === \">\") return typedAtom > typedComparisonValue\n else if (operator === \"<\") return typedAtom < typedComparisonValue\n else if (operator === \">=\") return typedAtom >= typedComparisonValue\n else if (operator === \"<=\") return typedAtom <= typedComparisonValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
7 days ago
package.json
Changed around line 19
- "scroll-cli": "^167.2.0",
+ "scroll-cli": "^167.3.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nhtmlSpanParser\n description HTML span tag.\n extends abstractHtmlElementParser\n cue span\n example\n span Hello\n javascript\n tag = \"span\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
7 days ago
ScrollHub.js
Changed around line 118: container
- button Refresh
+ scrollButton Refresh
package.json
Changed around line 1
- "version": "0.80.0",
+ "version": "0.81.0",
Changed around line 19
- "scroll-cli": "^166.3.1",
+ "scroll-cli": "^167.2.0",
prompts/website.scroll
Changed around line 1
- - Use only Scroll, vanilla HTML, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
- Create clean, semantic HTML5
- Make it mobile-responsive
- Follow modern best practices and accessibility guidelines
- Keep it simple but professional
- - Include basic SEO meta tags using Scroll
- Use only relative links and no external resources
- Do not put a copyright symbol or all rights reserved in the footer.
- Make it beautiful. Dazzling. Advanced used of CSS.
+ As a refresher, for doing the html body, Scroll is a whitespace based language that uses a single indented space to mark a line (aka particle) as a subparticle of a parent line.
+
+ For example:
+
+ header
+ class hero
+ nav
+ div Scroll
+ class logo
+ div
+ class nav-links
+ a Features
+ href #features
+ a Examples
+ href #examples
+ a Edit
+ href edit.html
+ a GitHub
+ class cta-button
+ href https://github.com/breck7/scroll
+ div
+ class hero-content
+ h1 Write Better with Scroll
+ p The extendible markup language that makes source beautiful and compiles to anything
+ a Get Started
+ class primary-button
+ href https://hub.scroll.pub/
+ main
+ section
+ id features
+ class features
+ h2 Why Scroll?
+ div
+ class feature-grid
+ div
+ class feature-card
+ div ⚡
+ class feature-icon
+ h3 Simple Syntax
+ p Like Markdown, but more powerful. No parentheses needed.
+ div
+ class feature-card
+ div 🧩
+ class feature-icon
+ h3 Extendible
+ p Build your own custom parsers.
+ div
+ class feature-card
+ div 🎨
+ class feature-icon
+ h3 Beautiful Output
+ p Create stunning documents with minimal effort.
+ div
+ class feature-card
+ div 🚀
+ class feature-icon
+ h3 Fast & Light
+ p Built on the efficient PPS Stack.
+ section
+ id examples
+ class code-demo
+ h2 See It In Action
+ div
+ class code-container
+ pre
+ class code-example
+ div
+ class code-output
+ footer
+ div
+ class footer-content
+ div
+ class footer-links
+ a Documentation
+ href https://scroll.pub/tutorial.html
+ a Community
+ href https://www.reddit.com/r/WorldWideScroll/
+ a Blog
+ href https://scroll.pub/blog
+ p Started by Breck Yunits. Evolved by a community.
+ https://twitter.com/breckyunits Breck Yunits
+ https://github.com/breck7/scroll/graphs/contributors community
+
+
Changed around line 102: metaTags
- body.html
+
+ (body content here. no blank lines please.)
+
- (HTML body content here)
public/.requests.scroll
Changed around line 8: container
- button Refresh
+ scrollButton Refresh
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nhtmlTypeAtom\n extends stringAtom\n paint constant.language\n enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range\n description HTML input type attribute values.\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cueFromId\n description A button.\n postParser\n description Post a particle.\n example\n scrollButton Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nhtmlFormParser\n description HTML form tag.\n extends abstractHtmlElementParser\n cue form\n example\n form\n input\n javascript\n tag = \"form\"\nhtmlInputParser\n description HTML input tag.\n extends abstractHtmlElementParser\n cue input\n example\n input\n type text\n placeholder Enter your name\n javascript\n tag = \"input\"\n isSelfClosing = true\nhtmlSelectParser\n description HTML select tag.\n extends abstractHtmlElementParser\n cue select\n example\n select\n option Value 1\n option Value 2\n javascript\n tag = \"select\"\nhtmlOptionParser\n description HTML option tag.\n extends abstractHtmlElementParser\n cue option\n example\n select\n option Choose an option\n option First Option\n javascript\n tag = \"option\"\nhtmlTextareaParser\n description HTML textarea tag.\n extends abstractHtmlElementParser\n cue textarea\n example\n textarea\n placeholder Enter your message\n rows 4\n javascript\n tag = \"textarea\"\nhtmlButtonParser\n description HTML button tag.\n extends abstractHtmlElementParser\n cue button\n example\n button Submit\n type submit\n javascript\n tag = \"button\"\nhtmlLabelParser\n description HTML label tag.\n extends abstractHtmlElementParser\n cue label\n example\n label Name\n for username\n javascript\n tag = \"label\"\nh1LiteralParser\n description HTML h1 tag.\n extends abstractHtmlElementParser\n cue h1\n example\n main\n h1 Title\n javascript\n tag = \"h1\"\nh2LiteralParser\n description HTML h2 tag.\n extends abstractHtmlElementParser\n cue h2\n example\n main\n h2 Title\n javascript\n tag = \"h2\"\nh3LiteralParser\n description HTML h3 tag.\n extends abstractHtmlElementParser\n cue h3\n example\n main\n h3 Title\n javascript\n tag = \"h3\"\nh4LiteralParser\n description HTML h4 tag.\n extends abstractHtmlElementParser\n cue h4\n example\n main\n h4 Title\n javascript\n tag = \"h4\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextValueParser\n popularity 0.000217\n cue value\n description Set HTML value attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextForParser\n popularity 0.000217\n cue for\n description Set HTML for attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextPlaceholderParser\n popularity 0.000217\n cue placeholder\n description Set HTML placeholder attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRowsParser\n popularity 0.000217\n cue rows\n description Set HTML rows attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextTypeParser\n popularity 0.000217\n cue type\n description Set HTML type attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlTypeAtom\naftertextAltParser\n popularity 0.000217\n cue alt\n description Set HTML alt attribute for images.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTitleParser\n popularity 0.000217\n cue title\n description Set HTML title attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextNameParser\n popularity 0.000217\n cue name\n description Set HTML name attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextMaxlengthParser\n popularity 0.000217\n cue maxlength\n description Set HTML maxlength attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom integerAtom\naftertextPatternParser\n popularity 0.000217\n cue pattern\n description Set HTML pattern attribute for input validation.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextRequiredParser\n popularity 0.000217\n cue required\n description Set HTML required attribute.\n extends abstractAftertextAttributeParser\n single\naftertextDisabledParser\n popularity 0.000217\n cue disabled\n description Set HTML disabled attribute.\n extends abstractAftertextAttributeParser\n single\naftertextReadonlyParser\n popularity 0.000217\n cue readonly\n description Set HTML readonly attribute.\n extends abstractAftertextAttributeParser\n single\naftertextAriaLabelParser\n popularity 0.000217\n cue aria-label\n description Set ARIA label for accessibility.\n extends abstractAftertextAttributeParser\n catchAllAtomType htmlAnyAtom\naftertextTargetParser\n popularity 0.000217\n cue target\n description Set HTML target attribute for links.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlAnyAtom\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"167.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
public/new.scroll
Changed around line 27: ScrollHub is your web app backend using the *New File API*.
- button Publish
+ scrollButton Publish
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.81.0 1/14/2025
+ 🎉 support for unlimited wildcard SSLs
+ 🎉 refactor of AI prompts
+ 🎉 AI prompt template set via query string
+ 🎉 AI prompt settings - adjust model and tld and save to local storage
+ 🎉 upgraded Scroll
+ 🎉 welcome message improvements
+
Breck Yunits
Breck Yunits
7 days ago
ScrollHub.js
Changed around line 1876: scrollVersionLink`
- const { certCache, pendingCerts, wildCardCerts } = this
+ const { certCache, pendingCerts } = this
Changed around line 1891: scrollVersionLink`
- const wildcardConfig = this.config.get("wildcard")
- if (!wildcardConfig) return
- const [pattern, certFile, keyFile] = wildcardConfig.split(" ")
-
- const sslOptions = {
- cert: fs.readFileSync(certFile, "utf8"),
- key: fs.readFileSync(keyFile, "utf8")
- }
- // Convert wildcard pattern to regex
- // e.g., "*.example.com" becomes "^[^.]+\.example\.com$"
- const regexPattern = pattern
- .replace(/\./g, "\\.") // Escape dots
- .replace(/\*/g, "[^.]+") // Replace * with regex for non-dot chars
- const regex = new RegExp(`^${regexPattern}$`)
-
- this.wildCardCerts.push({ regex, pattern, cert: sslOptions })
+ const wildcards = this.config.getParticles("wildcard")
+ if (!wildcards.length) return
+ wildcards.forEach(wildcardConfig => {
+ const [_, pattern, certFile, keyFile] = wildcardConfig.atoms
+ const sslOptions = {
+ cert: fs.readFileSync(certFile, "utf8"),
+ key: fs.readFileSync(keyFile, "utf8")
+ }
+ // Convert wildcard pattern to regex
+ // e.g., "*.example.com" becomes "^[^.]+\.example\.com$"
+ const regexPattern = pattern
+ .replace(/\./g, "\\.") // Escape dots
+ .replace(/\*/g, "[^.]+") // Replace * with regex for non-dot chars
+ const regex = new RegExp(`^${regexPattern}$`)
+
+ this.wildCardCerts.push({ regex, pattern, cert: sslOptions })
+ })
Breck Yunits
Breck Yunits
7 days ago
Agents.js
Changed around line 27: ${domainExpression}`
- basePrompt = basePrompt.replaceAll("DOMAIN_EXPRESSION", domainSuffix)
+ basePrompt = basePrompt.replaceAll("DOMAIN_EXPRESSION", domainExpression)
Breck Yunits
Breck Yunits
7 days ago
Agents.js
Changed around line 25: class FolderPrompt {
- basePrompt = basePrompt.replace("USER_PROMPT", userPrompt)
- basePrompt = basePrompt.replace("DOMAIN_PROMPT", domainPrompt)
- basePrompt = basePrompt.replace("DOMAIN_EXPRESSION", domainSuffix)
+ basePrompt = basePrompt.replaceAll("USER_PROMPT", userPrompt)
+ basePrompt = basePrompt.replaceAll("DOMAIN_PROMPT", domainPrompt)
+ basePrompt = basePrompt.replaceAll("DOMAIN_EXPRESSION", domainSuffix)
Breck Yunits
Breck Yunits
7 days ago
Agents.js
Changed around line 10: class FolderPrompt {
- this.domainSuffix = domainSuffix
+ this.domainSuffix = "." + domainSuffix.replace(/^\./, "")
Changed around line 19: class FolderPrompt {
- domainSuffix = "." + domainSuffix.replace(/^\./, "")
Changed around line 61: ${domainExpression}`
- // Ensure the suggested domain ends with .scroll.pub
- if (!suggestedDomain.endsWith(".scroll.pub")) {
- suggestedDomain = suggestedDomain.replace(/\.scroll\.pub.*$/, "") + ".scroll.pub"
- }
+ const { domainSuffix } = this
+ // Ensure the suggested domain ends with domainSuffix
+ if (!suggestedDomain.endsWith(domainSuffix)) suggestedDomain = suggestedDomain.replace(domainSuffix, "") + domainSuffix
- const baseName = suggestedDomain.replace(".scroll.pub", "")
- finalDomain = `${baseName}${counter}.scroll.pub`
+ const baseName = suggestedDomain.replace(domainSuffix, "")
+ finalDomain = `${baseName}${counter}${domainSuffix}`
Breck Yunits
Breck Yunits
7 days ago
public/promptSettings.js
Changed around line 1
+ // Constants for localStorage keys and selectors
+ const STORAGE_KEY = "promptSettings"
+ const SELECTORS = {
+ agentSelect: ".promptSettings select:first-child",
+ tldSelect: ".promptSettings select:last-child",
+ form: "#createForm",
+ agentInput: 'input[name="agent"]',
+ tldInput: 'input[name="tld"]'
+ }
+
+ // Settings handler class
+ class SettingsHandler {
+ constructor() {
+ this.agentSelect = document.querySelector(SELECTORS.agentSelect)
+ this.tldSelect = document.querySelector(SELECTORS.tldSelect)
+ this.form = document.querySelector(SELECTORS.form)
+ this.agentInput = this.form.querySelector(SELECTORS.agentInput)
+ this.tldInput = this.form.querySelector(SELECTORS.tldInput)
+
+ this.initializeEventListeners()
+ this.rehydrateSettings()
+ }
+
+ initializeEventListeners() {
+ // Handle agent selection change
+ this.agentSelect.addEventListener("change", e => {
+ const agent = e.target.value.toUpperCase()
+ this.agentInput.value = agent
+ this.saveSettings()
+ })
+
+ // Handle TLD selection change
+ this.tldSelect.addEventListener("change", e => {
+ const tld = e.target.value
+ this.tldInput.value = tld
+ this.saveSettings()
+ })
+ }
+
+ saveSettings() {
+ const settings = {
+ agent: this.agentSelect.value,
+ tld: this.tldSelect.value
+ }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings))
+ }
+
+ rehydrateSettings() {
+ try {
+ const savedSettings = localStorage.getItem(STORAGE_KEY)
+ if (savedSettings) {
+ const settings = JSON.parse(savedSettings)
+
+ // Restore agent selection
+ if (settings.agent) {
+ this.agentSelect.value = settings.agent
+ this.agentInput.value = settings.agent.toUpperCase()
+ }
+
+ // Restore TLD selection
+ if (settings.tld) {
+ this.tldSelect.value = settings.tld
+ this.tldInput.value = settings.tld
+ }
+ }
+ } catch (error) {
+ console.error("Error rehydrating settings:", error)
+ }
+ }
+ }
+
+ // Initialize settings handler when DOM is ready
+ document.addEventListener("DOMContentLoaded", () => {
+ new SettingsHandler()
+ })
Breck Yunits
Breck Yunits
7 days ago
public/promptSettings.scroll
Changed around line 17: css
- bottom: 10px;
+ top: 10px;
Breck Yunits
Breck Yunits
7 days ago
refactor prompts. make ai agent and tld dropdowns
public/ai.scroll
Changed around line 1
- claude.scroll
+ indexTop.scroll
+ replace AGENT Claude
+ replace TEMPLATE website
+ replace TLD scroll.pub
+ createFromPrompt.scroll
+ indexBottom.scroll
public/claude.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT Claude
- replace TEMPLATE website
- createFromPrompt.scroll
- indexBottom.scroll
public/claudeBlog.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT Claude
- replace TEMPLATE blog
- createFromPrompt.scroll
- indexBottom.scroll
public/claudeDatabase.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT Claude
- replace TEMPLATE database
- createFromPrompt.scroll
- indexBottom.scroll
public/claudeSlideshow.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT Claude
- replace TEMPLATE slideshow
- createFromPrompt.scroll
- indexBottom.scroll
public/createFromPrompt.scroll
Changed around line 2
+
Changed around line 13: inlineJs create.js
-

While you wait, say hi on X

+

While you wait, say hi on X

+
+
+ script
+ {const template = new URLSearchParams(window.location.search).get('template');
+ if (template) {
+ document.querySelector('[name="template"]').value = template;
+ document.querySelector('.createButton').textContent = `Create ${template}`;
+ document.querySelector('#folderName').placeholder = `Describe the ${template} you want`;
+ document.querySelector('.modal-content p').textContent = `Creating your ${template}...`;
+ }
+ }
+
Changed around line 68: script
-
+
+
+ promptSettings.scroll
public/createLinks.scroll
Changed around line 1
- claude.html Website
- deepseekBlog.html Blog
- claudeSlideshow.html Slideshow
- claudeDatabase.html Database
+ ai.html?template=website Website
+ ai.html?template=blog Blog
+ ai.html?template=slideshow Slideshow
+ ai.html?template=database Database
public/deepseek.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT DeepSeek
- replace TEMPLATE website
- createFromPrompt.scroll
- indexBottom.scroll
public/deepseekBlog.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT DeepSeek
- replace TEMPLATE blog
- createFromPrompt.scroll
- indexBottom.scroll
public/deepseekSlideshow.scroll
Changed around line 0
- indexTop.scroll
- replace AGENT DeepSeek
- replace TEMPLATE slideshow
- createFromPrompt.scroll
- indexBottom.scroll
public/promptSettings.scroll
Changed around line 1
+ css
+ select {
+ border: none;
+ border-radius: 4px;
+ background: transparent;
+ appearance: none;
+ cursor: pointer;
+ color: rgba(144, 144, 144, 0.8);
+ margin-right: 10px;
+ }
+ select:focus {
+ outline: none;
+ }
+ /* Hide default arrow in IE */
+ select::-ms-expand {
+ display: none;
+ }
+ .promptSettings {
+ position: fixed;
+ bottom: 10px;
+ right: 5px;
+ font-size: 12px;
+ }
+
+
+
+
+
+
+ inlineJs promptSettings.js
Breck Yunits
Breck Yunits
7 days ago
public/createForm.scroll
Changed around line 1
-
+
public/createFromPrompt.scroll
Changed around line 2
-
+
Breck Yunits
Breck Yunits
7 days ago
Agents.js
Changed around line 5: const OpenAI = require("openai")
- constructor(userPrompt, existingFolderNames, agent, whatKind) {
+ constructor(userPrompt, existingFolderNames, agent, whatKind, domainSuffix) {
- this.systemPrompt = fs.readFileSync(path.join(__dirname, "prompts", whatKind + ".scroll"), "utf8").replace("USER_PROMPT", userPrompt)
+ this.domainSuffix = domainSuffix
+ this.systemPrompt = this.makePrompt(userPrompt, domainSuffix)
+ makePrompt(userPrompt, domainSuffix) {
+ domainSuffix = "." + domainSuffix.replace(/^\./, "")
+ const domainExpression = `(domain${domainSuffix} here)`
+ const domainPrompt = `First suggest a short, memorable domain name ending in ${domainSuffix} that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ ${domainExpression}`
+ let basePrompt = fs.readFileSync(path.join(__dirname, "prompts", this.what + ".scroll"), "utf8")
+ basePrompt = basePrompt.replace("USER_PROMPT", userPrompt)
+ basePrompt = basePrompt.replace("DOMAIN_PROMPT", domainPrompt)
+ basePrompt = basePrompt.replace("DOMAIN_EXPRESSION", domainSuffix)
+ return basePrompt
+ }
+
Changed around line 172: class Agents {
- async createFolderNameAndFilesFromPrompt(userPrompt, existingNames, agentName, promptTemplate = "website") {
+ async createFolderNameAndFilesFromPrompt(userPrompt, existingNames, agentName, promptTemplate, domainSuffix) {
- const prompt = new FolderPrompt(userPrompt, existingNames, agent, promptTemplate)
+ const prompt = new FolderPrompt(userPrompt, existingNames, agent, promptTemplate, domainSuffix)
ScrollHub.js
Changed around line 732: If you'd like to create this folder, visit our main site to get started.
+ const domainSuffix = req.body.tld || "scroll.pub"
- const response = await agents.createFolderNameAndFilesFromPrompt(prompt, existingNames, agent, template)
+ const response = await agents.createFolderNameAndFilesFromPrompt(prompt, existingNames, agent, template, domainSuffix)
prompts/blog.scroll
Changed around line 12: Requirements:
- Do not put a copyright symbol or all rights reserved in the footer.
- Make it beautiful. Dazzling. Advanced used of CSS.
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
+ DOMAIN_PROMPT
Changed around line 36: buildHtml
- baseUrl https://(domain.scroll.pub here)
+ baseUrl https://DOMAIN_EXPRESSION
- editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ editBaseUrl /edit.html?folderName=DOMAIN_EXPRESSION&fileName=
prompts/database.scroll
Changed around line 62: Requirements:
- Do not put a copyright symbol or all rights reserved in the footer.
- Make it beautiful. Dazzling. Advanced used of CSS.
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
+ DOMAIN_PROMPT
Changed around line 81: buildHtml
- editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ editBaseUrl /edit.html?folderName=DOMAIN_EXPRESSION&fileName=
prompts/slideshow.scroll
Changed around line 31: slideshowParser
- First suggest a short, memorable domain name ending in .scroll.pub that represents this slideshow. Then provide the files. Use this exact format:
-
- (domain.scroll.pub here)
+ DOMAIN_PROMPT
prompts/website.scroll
Changed around line 11: Requirements:
- Do not put a copyright symbol or all rights reserved in the footer.
- Make it beautiful. Dazzling. Advanced used of CSS.
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
+ DOMAIN_PROMPT
- baseUrl https://(domain.scroll.pub here)
+ baseUrl https://DOMAIN_EXPRESSION
Breck Yunits
Breck Yunits
8 days ago
Improve welcome message and refactor prompts
Agents.js
Changed around line 4: const path = require("path")
- class AbstractPrompt {
- constructor(userPrompt, existingNames, agent, what) {
+ class FolderPrompt {
+ constructor(userPrompt, existingFolderNames, agent, whatKind) {
- this.existingNames = existingNames
+ this.existingNames = existingFolderNames
- this.what = what
+ this.what = whatKind
+ this.systemPrompt = fs.readFileSync(path.join(__dirname, "prompts", whatKind + ".scroll"), "utf8").replace("USER_PROMPT", userPrompt)
Changed around line 71: class AbstractPrompt {
- class SimpleCreationPrompt extends AbstractPrompt {
- get systemPrompt() {
- return `You are an expert web developer. Create a website based on this request: "${this.userPrompt}"
-
- Requirements:
- - Use only Scroll, vanilla HTML, CSS, and JavaScript (NO frameworks, NO external dependencies)
- - Create clean, semantic HTML5
- - Make it mobile-responsive
- - Follow modern best practices and accessibility guidelines
- - Keep it simple but professional
- - Include basic SEO meta tags using Scroll
- - Use only relative links and no external resources
- - Do not put a copyright symbol or all rights reserved in the footer.
- - Make it beautiful. Dazzling. Advanced used of CSS.
-
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
- buildHtml
- baseUrl https://(domain.scroll.pub here)
- metaTags
- editButton /edit.html
- title (Title here)
- style.css
- body.html
- script.js
- (HTML body content here)
- (CSS content here)
- (JavaScript content here)
- }
- }
-
- class DatabasePrompt extends SimpleCreationPrompt {
- get systemPrompt() {
- return `You are an expert web, scroll, and knowledge base developer. Create a knowledge base using ScrollSets based on this request: "${this.userPrompt}"
-
- ScrollSets use the Particles, Parsers, and Scroll stack to define measures, and then encode concepts using those measures.
-
- For example:
-
- idParser
- extends abstractIdParser
- organismParser
- extends abstractStringMeasureParser
- description The organism name mainly associated with the organelle such as human, plant, whale, etc.
- diameterParser
- extends abstractIntegerMeasureParser
- description The diameter of the organelle in nanometers
- lowParser
- extends abstractIntegerMeasureParser
- description For cells that have this kind of organelle, how many are usually found on the low end?
- medianParser
- extends abstractIntegerMeasureParser
- description For cells that have this kind of organelle, how many are usually found in the median?
- highParser
- extends abstractIntegerMeasureParser
- description For cells that have this kind of organelle, how many are usually found on the high end?
-
- measures.parsers
-
- id Mitochondria
- organism human
- diameter 1000
- low 200
- median 500
- high 2000
-
- id Chloroplast
- organism plant
- diameter 6000
- low 20
- median 40
- high 100
-
- id Nucleus
- organism human
- diameter 6000
- low 1
- median 1
- high 2
-
-
- Requirements:
- - Create 5 - 7 measures. The most important things about this topic.
- - Write 5 concepts. The most important concepts in this topic.
- - Build a nice homepage iterating over the data.
- - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
- - Create clean, semantic HTML5
- - Make it mobile-responsive
- - Follow modern best practices and accessibility guidelines
- - Keep it simple but professional
- - Use only relative links and no external resources
- - Do not put a copyright symbol or all rights reserved in the footer.
- - Make it beautiful. Dazzling. Advanced used of CSS.
-
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
- (put measures here)
- (put concepts here)
- title (database title here)
- header.scroll
- (content here)
- concepts.scroll
- concepts
- printTable
- footer.scroll
- importOnly
- buildHtml
- homeButton
- metaTags
- editButton
- editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
- metaTags
- style.css
- script.js
- importOnly
- center
- scrollVersionLink
- (CSS content here)
- (JavaScript content here)
- }
- }
-
- class BlogCreationPrompt extends SimpleCreationPrompt {
- get systemPrompt() {
- return `You are an expert web developer. Create a blog based on this request: "${this.userPrompt}"
-
- Requirements:
- - Write 2 blog posts. Keep them short. Intelligent. Data backed. Witty.
- - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
- - Create clean, semantic HTML5
- - Make it mobile-responsive
- - Follow modern best practices and accessibility guidelines
- - Keep it simple but professional
- - Include basic SEO meta tags using Scroll
- - Use only relative links and no external resources
- - Do not put a copyright symbol or all rights reserved in the footer.
- - Make it beautiful. Dazzling. Advanced used of CSS.
-
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
- title (blog title here)
- header.scroll
- printSnippets All
- footer.scroll
- title (first post title here)
- tags All
- header.scroll
- (first post content here)
- footer.scroll
- title (second post title here)
- tags All
- header.scroll
- (second post content here)
- footer.scroll
- importOnly
- buildHtml
- buildTxt
- homeButton
- leftRightButtons
- baseUrl https://(domain.scroll.pub here)
- metaTags
- editButton
- editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
- metaTags
- style.css
- script.js
- container
- printTitle
- buildRss
- printFeed All
- importOnly
- center
- scrollVersionLink
- (CSS content here)
- (JavaScript content here)
- }
- }
-
- class SlideshowCreationPrompt extends SimpleCreationPrompt {
- get systemPrompt() {
- return `You are an expert design agency tasked with helping someone create a slideshow. Create a slideshow based on this request: "${this.userPrompt}"
-
- Requirements:
- - Make the slideshow inspirational, concise, witty.
- - The scroll slideshow keyword automatically injects javascript to handle the showing/hiding of slide content, navigation, urls, etc. You just need to add content and style.
- - Use mostly Scroll, but you can also use CSS, HTML and JavaScript (NO frameworks, NO external dependencies)
- - Make it mobile-responsive
- - 5-10 slides
- - Follow modern best practices and accessibility guidelines
- - Keep it simple but professional
- - Use only relative links and no external resources
- - Do not put a copyright symbol or all rights reserved or confidential or any of that mumbo jumbo.
- - Make it beautiful. Dazzling. Advanced used of CSS.
-
- Below is the Scroll Parser that implements the Slideshow. This should give you the class names for the styles.
- slideshowParser
- // Left and right arrows navigate.
- description Slideshow widget. *** delimits slides.
- extends abstractScrollWithRequirementsParser
- string copyFromExternal .jquery-3.7.1.min.js .slideshow.js
- example
- slideshow
- Why did the cow cross the road?
- ***
- Because it wanted to go to the MOOOO-vies.
- ***
- THE END
- ****
- javascript
- buildHtml() {
- return \`
\`
- }
-
- First suggest a short, memorable domain name ending in .scroll.pub that represents this slideshow. Then provide the files. Use this exact format:
-
- (domain.scroll.pub here)
- buildTxt
- buildHtml
- title (slideshow title for meta tags)
- metaTags
- style.css
- script.js
- slideshow
- ***
-
- (first slide)
-
- ***
-
- (slides here, delimited by ***)
-
- ***
-
- (Final slide)
-
- ****
- (CSS content here)
- (Any JavaScript content here, if needed)
- }
- }
-
Changed around line 157: class Agents {
- availablePrompts = {
- website: SimpleCreationPrompt,
- blog: BlogCreationPrompt,
- slideshow: SlideshowCreationPrompt,
- database: DatabasePrompt
- }
-
- const promptToUse = this.availablePrompts[promptTemplate]
- const prompt = new promptToUse(userPrompt, existingNames, agent, promptTemplate)
+ const prompt = new FolderPrompt(userPrompt, existingNames, agent, promptTemplate)
prompts/blog.scroll
Changed around line 1
+ You are an expert web developer. Create a blog based on this request: "USER_PROMPT"
+
+ Requirements:
+ - Write 2 blog posts. Keep them short. Intelligent. Data backed. Witty.
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Include basic SEO meta tags using Scroll
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ title (blog title here)
+ header.scroll
+ printSnippets All
+ footer.scroll
+ ---(firstPostPermalinkHere).scroll---
+ title (first post title here)
+ tags All
+ header.scroll
+ (first post content here)
+ footer.scroll
+ ---(secondPostPermalinkHere).scroll---
+ title (second post title here)
+ tags All
+ header.scroll
+ (second post content here)
+ footer.scroll
+ ---header.scroll---
+ importOnly
+ buildHtml
+ buildTxt
+ homeButton
+ leftRightButtons
+ baseUrl https://(domain.scroll.pub here)
+ metaTags
+ editButton
+ editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ metaTags
+ style.css
+ script.js
+ container
+ printTitle
+ ---feed.scroll---
+ buildRss
+ printFeed All
+ ---footer.scroll---
+ importOnly
+ center
+ scrollVersionLink
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---
prompts/database.scroll
Changed around line 1
+ You are an expert web, scroll, and knowledge base developer. Create a knowledge base using ScrollSets based on this request: "USER_PROMPT"
+
+ ScrollSets use the Particles, Parsers, and Scroll stack to define measures, and then encode concepts using those measures.
+
+ For example:
+
+ ---measures.parsers---
+ idParser
+ extends abstractIdParser
+ organismParser
+ extends abstractStringMeasureParser
+ description The organism name mainly associated with the organelle such as human, plant, whale, etc.
+ diameterParser
+ extends abstractIntegerMeasureParser
+ description The diameter of the organelle in nanometers
+ lowParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found on the low end?
+ medianParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found in the median?
+ highParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found on the high end?
+
+ ---concepts.scroll---
+ measures.parsers
+
+ id Mitochondria
+ organism human
+ diameter 1000
+ low 200
+ median 500
+ high 2000
+
+ id Chloroplast
+ organism plant
+ diameter 6000
+ low 20
+ median 40
+ high 100
+
+ id Nucleus
+ organism human
+ diameter 6000
+ low 1
+ median 1
+ high 2
+
+ ---
+
+ Requirements:
+ - Create 5 - 7 measures. The most important things about this topic.
+ - Write 5 concepts. The most important concepts in this topic.
+ - Build a nice homepage iterating over the data.
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---measures.parsers---
+ (put measures here)
+ ---concepts.scroll---
+ (put concepts here)
+ ---index.scroll---
+ title (database title here)
+ header.scroll
+ (content here)
+ concepts.scroll
+ concepts
+ printTable
+ footer.scroll
+ ---header.scroll---
+ importOnly
+ buildHtml
+ homeButton
+ metaTags
+ editButton
+ editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ metaTags
+ style.css
+ script.js
+ ---footer.scroll---
+ importOnly
+ center
+ scrollVersionLink
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---
prompts/slideshow.scroll
Changed around line 1
+ You are an expert design agency tasked with helping someone create a slideshow. Create a slideshow based on this request: "USER_PROMPT"
+
+ Requirements:
+ - Make the slideshow inspirational, concise, witty.
+ - The scroll slideshow keyword automatically injects javascript to handle the showing/hiding of slide content, navigation, urls, etc. You just need to add content and style.
+ - Use mostly Scroll, but you can also use CSS, HTML and JavaScript (NO frameworks, NO external dependencies)
+ - Make it mobile-responsive
+ - 5-10 slides
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved or confidential or any of that mumbo jumbo.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ Below is the Scroll Parser that implements the Slideshow. This should give you the class names for the styles.
+ slideshowParser
+ // Left and right arrows navigate.
+ description Slideshow widget. *** delimits slides.
+ extends abstractScrollWithRequirementsParser
+ string copyFromExternal .jquery-3.7.1.min.js .slideshow.js
+ example
+ slideshow
+ Why did the cow cross the road?
+ ***
+ Because it wanted to go to the MOOOO-vies.
+ ***
+ THE END
+ ****
+ javascript
+ buildHtml() {
+ return \`
\`
+ }
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this slideshow. Then provide the files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ buildTxt
+ buildHtml
+ title (slideshow title for meta tags)
+ metaTags
+ style.css
+ script.js
+ slideshow
+ ***
+
+ (first slide)
+
+ ***
+
+ (slides here, delimited by ***)
+
+ ***
+
+ (Final slide)
+
+ ****
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (Any JavaScript content here, if needed)
+ ---end---
prompts/website.scroll
Changed around line 1
+ You are an expert web developer. Create a website based on this request: "USER_PROMPT"
+
+ Requirements:
+ - Use only Scroll, vanilla HTML, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Include basic SEO meta tags using Scroll
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ buildHtml
+ baseUrl https://(domain.scroll.pub here)
+ metaTags
+ editButton /edit.html
+ title (Title here)
+ style.css
+ body.html
+ script.js
+ ---body.html---
+ (HTML body content here)
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---
public/scrollHubEditor.js
Changed around line 455: class EditorApp {
+ document.querySelector("#theModal").classList.remove("scrollModalFit")
Changed around line 697: ${this.folderName} is live!
- Visit now
+ Visit ${this.folderName}
- Scroll and ScrollHub are tools to help you refine and publish your best ideas.
+ We make Scroll and ScrollHub to help you refine and publish your best ideas.
- Email me (breck@scroll.pub) with any requests or feedback.
-
- -Breck
- Follow me on X or GitHub
+ I'd love to hear your requests and feedback! Contact me on X, GitHub, or email.
+ email breck@scroll.pub email
+ target _blank
- target _blank`
+ target _blank
+
+ -Breck`
+ document.querySelector("#theModal").classList.add("scrollModalFit")
public/scrollHubStyle.css
Changed around line 102: body {
+ .scrollModalFit {
+ left: calc((100% - 600px) / 2) !important;
+ right: calc((100% - 600px) / 2) !important;
+ bottom: unset !important;
+ width: 600px;
+ }
+
Breck Yunits
Breck Yunits
8 days ago
public/readme.scroll
Changed around line 22: Optional steps:
+ # Wildcard SSL certs
+ code
+ sudo certbot certonly --manual --preferred-challenges dns -d example.com -d "*.example.com"
+ dig _acme-challenge.example.com TXT
+
Breck Yunits
Breck Yunits
8 days ago
package.json
Changed around line 19
- "scroll-cli": "^166.3.0",
+ "scroll-cli": "^166.3.1",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
8 days ago
package.json
Changed around line 19
- "scroll-cli": "^166.2.0",
+ "scroll-cli": "^166.3.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractIndentableParagraphParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractIndentableParagraphParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractIndentableParagraphParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractIndentableParagraphParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const selfClose = this.isSelfClosing ? \" /\" : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}${selfClose}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n if (this.isSelfClosing) return \"\"\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nabstractHtmlElementParser\n extends abstractIndentableParagraphParser\n javascript\n defaultClassName = \"\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractHtmlElementParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractHtmlElementParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractHtmlElementParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractHtmlElementParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractHtmlElementParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractHtmlElementParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractHtmlElementParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractHtmlElementParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractHtmlElementParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractHtmlElementParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\n isSelfClosing = true\nhtmlAParser\n description HTML a tag.\n extends abstractHtmlElementParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextSrcParser\n extends aftertextHrefParser\n cue src\n description Set HTML src attribute.\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
8 days ago
package.json
Changed around line 19
- "scroll-cli": "^166.1.0",
+ "scroll-cli": "^166.2.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractIndentableParagraphParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractIndentableParagraphParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractIndentableParagraphParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.1.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractIndentableParagraphParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractIndentableParagraphParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractIndentableParagraphParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlImgParser\n description HTML img tag.\n extends abstractIndentableParagraphParser\n cue img\n example\n img foo.png\n javascript\n tag = \"img\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
8 days ago
package.json
Changed around line 19
- "scroll-cli": "^166.0.0",
+ "scroll-cli": "^166.1.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlUlParser\n description HTML ul tag.\n extends abstractIndentableParagraphParser\n cue ul\n example\n ul\n li A list\n javascript\n tag = \"ul\"\nhtmlOlParser\n description HTML ol tag.\n extends abstractIndentableParagraphParser\n cue ol\n example\n ol\n li A list\n javascript\n tag = \"ol\"\nhtmlLiParser\n description HTML li tag.\n extends abstractIndentableParagraphParser\n cue li\n example\n ol\n li A list\n javascript\n tag = \"li\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.1.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
8 days ago
ScrollHub.js
Changed around line 1928: scrollVersionLink`
- await certMaker.makeCertificate(domain, email, path.join(this.rootFolder, folderName))
+ await certMaker.makeCertificate(domain, email, path.join(this.rootFolder, domain))
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 210: class ScrollHub {
- process.title = process.title + " - ScrollHubProcess"
+ const { rootFolder } = this
+ const lastFolder = rootFolder.split("/").pop()
+ process.title = process.title + ` ScrollHub ${lastFolder}`
cli.js
Changed around line 23: class ScrollHubCLI extends SimpleCLI {
- const command = "ps aux | grep '[S]crollHubProcess' | awk '{printf \"%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s %s %s\\n\", $2, $3, $4, $5, $6, $9, $11, $12, $13}'"
+ const command = "ps aux | grep '[S]crollHub' | awk '{printf \"%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s %s %s\\n\", $2, $3, $4, $5, $6, $9, $11, $12, $13}'"
package.json
Changed around line 1
- "version": "0.79.0",
+ "version": "0.80.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.80.0 1/13/2025
+ 🎉 show hidden files toggle
+
public/scrollHubEditor.js
Changed around line 318: class EditorApp {
- if (!this.files) await this.refreshFileListCommand()
+ if (!this.allFiles) await this.refreshFileListCommand()
Changed around line 586: command+. toggleFocusModeCommand Editor
+ command+shift+h showHiddenFilesCommand Editor
Changed around line 609: nokey1 showWelcomeMessageCommand Help`
+ showHiddenFilesCommand() {
+ this.showHiddenFiles = !this.showHiddenFiles
+ delete this._files
+ this.renderFileList()
+ }
+
Changed around line 953: Follow me on X or GitHub
+ showHiddenFiles = false
+ get files() {
+ if (this._files) return this._files
+ const { allFiles, showHiddenFiles } = this
+ const filtered = {}
+ Object.keys(allFiles).forEach(key => {
+ const value = allFiles[key]
+ if (showHiddenFiles || value.tracked || !key.startsWith(".")) filtered[key] = value
+ })
+
+ this._files = filtered
+ return this._files
+ }
+
Changed around line 974: Follow me on X or GitHub
+ delete this._files
+ this.allFiles = allFiles
-
- const filtered = {}
- Object.keys(allFiles).forEach(key => {
- const value = allFiles[key]
- if (value.tracked || !key.startsWith(".")) filtered[key] = value
- })
-
- this.files = filtered
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 772: If you'd like to create this folder, visit our main site to get started.
+ initStampRoute() {
+ const { app, rootFolder, folderCache } = this
+
+ // Text file extensions - only include actual text formats
+ const textExtensions = new Set("scroll parsers txt html htm rb php md perl py mjs css json csv tsv psv ssv js".split(" "))
+
+ const isTextFile = filepath => {
+ const ext = path.extname(filepath).toLowerCase().slice(1)
+ return textExtensions.has(ext)
+ }
+
+ async function makeStamp(dir) {
+ let stamp = "stamp\n"
+
+ const handleFile = async (indentation, relativePath, itemPath) => {
+ if (!isTextFile(itemPath)) return
+ stamp += `${indentation}${relativePath}\n`
+ const content = await fsp.readFile(itemPath, "utf8")
+ stamp += `${indentation} ${content.replace(/\n/g, `\n${indentation} `)}\n`
+ }
+
+ let gitTrackedFiles
+
+ async function processDirectory(currentPath, depth) {
+ const items = await fsp.readdir(currentPath)
+
+ for (const item of items) {
+ const itemPath = path.join(currentPath, item)
+ const relativePath = path.relative(dir, itemPath)
+
+ if (!gitTrackedFiles.has(relativePath)) continue
+
+ const stats = await fsp.stat(itemPath)
+ const indentation = " ".repeat(depth)
+
+ if (stats.isDirectory()) {
+ stamp += `${indentation}${relativePath}/\n`
+ await processDirectory(itemPath, depth + 1)
+ } else if (stats.isFile()) {
+ await handleFile(indentation, relativePath, itemPath)
+ }
+ }
+ }
+
+ const stats = await fsp.stat(dir)
+ if (stats.isDirectory()) {
+ // Get list of git-tracked files
+ const { stdout } = await execAsync("git ls-files", { cwd: dir })
+ gitTrackedFiles = new Set(stdout.split("\n").filter(Boolean))
+ await processDirectory(dir, 1)
+ } else {
+ await handleFile(" ", dir, dir)
+ }
+
+ return stamp.trim()
+ }
+
+ app.get("/stamp", async (req, res) => {
+ const folderName = this.getFolderName(req)
+ const { rootFolder, folderCache } = this
+
+ // Check if folder exists
+ if (!folderCache[folderName]) return res.status(404).send("Folder not found")
+
+ const folderPath = path.join(rootFolder, folderName)
+
+ try {
+ // Generate the stamp
+ const stamp = await makeStamp(folderPath)
+
+ // Set content type and send response
+ res.setHeader("Content-Type", "text/plain; charset=utf-8")
+ res.send(stamp)
+ } catch (error) {
+ console.error("Error generating stamp:", error)
+ res.status(500).send(`Error generating stamp: ${error.toString().replace(/
+ }
+ })
+ }
+
Changed around line 895: If you'd like to create this folder, visit our main site to get started.
+ this.initStampRoute()
+
Changed around line 1765: scrollVersionLink`
- reservedExtensions = "scroll parsers txt html htm rb php perl py mjs css json csv tsv psv ssv pdf js jpg jpeg png gif webp svg heic ico mp3 mp4 mov mkv ogg webm ogv woff2 woff ttf otf tiff tif bmp eps git".split(" ")
+ reservedExtensions = "scroll parsers txt html htm md rb php perl py mjs css json csv tsv psv ssv pdf js jpg jpeg png gif webp svg heic ico mp3 mp4 mov mkv ogg webm ogv woff2 woff ttf otf tiff tif bmp eps git".split(" ")
package.json
Changed around line 1
- "version": "0.78.0",
+ "version": "0.79.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.79.0 1/13/2025
+ 🎉 wildcard cert support
+ 🎉 new /stamp route and command in editor
+ 🏥 various fixes
+
public/scrollHubEditor.js
Changed around line 581: ctrl+n createFileCommand File
+ nokey2 exportForPromptCommand Folder
Changed around line 675: nokey1 showWelcomeMessageCommand Help`
+ async exportForPromptCommand(event) {
+ this.openIframeModal("/stamp?folderName=" + this.folderName, event)
+ }
+
Changed around line 869: Follow me on X or GitHub
- openIframeModal(event) {
- const url = event.currentTarget.href
+ openIframeModalFromClick(event) {
+ this.openIframeModal(event.currentTarget.href, event)
+ }
+
+ openIframeModal(url, event) {
Changed around line 881: Follow me on X or GitHub
-
Changed around line 888: Follow me on X or GitHub
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 191: class ScrollHub {
+ this.wildCardCerts = []
Changed around line 1831: scrollVersionLink`
- this.wildCardCerts = []
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 764: If you'd like to create this folder, visit our main site to get started.
- return hasSslCert ? hasSslCert : this.getMatchingWildcardCert(hostname)
+ return hasSslCert ? hasSslCert : this.getMatchingWildcardCert(folderName)
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 764: If you'd like to create this folder, visit our main site to get started.
- return hasSslCert
+ return hasSslCert ? hasSslCert : this.getMatchingWildcardCert(hostname)
Changed around line 1781: scrollVersionLink`
+ getMatchingWildcardCert(hostname) {
+ const { wildCardCerts } = this
+ // Check for matching wildcard certificate
+ for (const wild of wildCardCerts) {
+ if (wild.regex.test(hostname)) return wild.cert
+ }
+ }
+
Changed around line 1796: scrollVersionLink`
- // Check for matching wildcard certificate
- for (const wild of wildCardCerts) {
- if (wild.regex.test(hostname)) return wild.cert
- }
+ const wild = this.getMatchingWildcardCert(hostname)
+ if (wild) return wild
Changed around line 1820: scrollVersionLink`
- this.wildCardCerts.push({ regex, pattern, cert })
+ this.wildCardCerts.push({ regex, pattern, cert: sslOptions })
Breck Yunits
Breck Yunits
9 days ago
ScrollHub.js
Changed around line 1782: scrollVersionLink`
- const { certCache, pendingCerts } = this
+ const { certCache, pendingCerts, wildCardCerts } = this
+
+ // Check for matching wildcard certificate
+ for (const wild of wildCardCerts) {
+ if (wild.regex.test(hostname)) return wild.cert
+ }
+
+ async loadWildCardCerts() {
+ const wildcardConfig = this.config.get("wildcard")
+ if (!wildcardConfig) return
+ const [pattern, certFile, keyFile] = wildcardConfig.split(" ")
+
+ const sslOptions = {
+ cert: fs.readFileSync(certFile, "utf8"),
+ key: fs.readFileSync(keyFile, "utf8")
+ }
+ // Convert wildcard pattern to regex
+ // e.g., "*.example.com" becomes "^[^.]+\.example\.com$"
+ const regexPattern = pattern
+ .replace(/\./g, "\\.") // Escape dots
+ .replace(/\*/g, "[^.]+") // Replace * with regex for non-dot chars
+ const regex = new RegExp(`^${regexPattern}$`)
+
+ this.wildCardCerts.push({ regex, pattern, cert })
+ }
+
Changed around line 1824: scrollVersionLink`
+ this.wildCardCerts = []
+ await this.loadWildCardCerts()
+
Breck Yunits
Breck Yunits
9 days ago
package.json
Changed around line 1
- "version": "0.77.0",
+ "version": "0.78.0",
public/edit.scroll
Changed around line 12: libs.js
-
+
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.78.0 1/12/2025
+ 🎉 better new site landing experience
+ 🎉 upgrade scroll
+
public/scrollHubEditor.js
Changed around line 679: nokey1 showWelcomeMessageCommand Help`
+ center
- link ${this.permalink} ${this.folderName}
+
+
+ center
+ Visit now
+ link ${this.permalink}
Changed around line 828: Follow me on X or GitHub
- this.previewIFrame = document.getElementById("previewIFrame")
+ this.previewIFrame = document.querySelector(".previewIFrame")
public/scrollHubStyle.css
Changed around line 102: body {
- #previewIFrame {
+ .previewIFrame,
+ .visitIframe {
Changed around line 111: body {
- #folderName {
- outline: none;
- }
-
- .iframeHolder {
+ .iframeHolder,
+ .iframeHolder2 {
Changed around line 123: body {
+ .visitIframe {
+ height: 1800px;
+ width: 1200px;
+ }
+
+ .iframeHolder2 {
+ width: 400px;
+ height: 250px;
+ }
+
+ #folderName {
+ outline: none;
+ }
+
Changed around line 356: textarea {
- padding: 4px 8px;
+ padding: 8px 16px;
+ margin: 10px;
+ display: inline-block;
+ text-decoration: none;
Breck Yunits
Breck Yunits
9 days ago
package.json
Changed around line 19
- "scroll-cli": "^165.4.0",
+ "scroll-cli": "^166.0.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollSectionParser\n popularity 0.007524\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nnamedColorAtom\n extends colorAtom\n enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\ncolumnNameOrColorAtom\n extends columnNameAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nhtmlSectionParser\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nhtmlHeaderParser\n description HTML header tag.\n extends abstractIndentableParagraphParser\n cue header\n example\n header\n # Title\n javascript\n tag = \"header\"\nhtmlFooterParser\n description HTML footer tag.\n extends abstractIndentableParagraphParser\n cue footer\n example\n footer\n p Made with love\n javascript\n tag = \"footer\"\nhtmlMainParser\n description HTML main tag.\n extends abstractIndentableParagraphParser\n cue main\n example\n main\n # Title\n javascript\n tag = \"main\"\nhtmlNavParser\n description HTML nav tag.\n extends abstractIndentableParagraphParser\n cue nav\n example\n nav\n a Home\n href /\n javascript\n tag = \"nav\"\nhtmlPreParser\n description HTML pre tag.\n extends abstractIndentableParagraphParser\n cue pre\n example\n pre\n javascript\n tag = \"pre\"\nhtmlAParser\n description HTML a tag.\n extends abstractIndentableParagraphParser\n cue a\n example\n a Home\n href /\n javascript\n tag = \"a\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue fileNav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Compile to RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Compile concepts to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Compile measures to delimited files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n inScope importToFooterParser abstractCommentParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\ntoFooterParser\n extends abstractScrollParser\n description Experimental way to move a section to the footer.\n atoms preBuildCommandAtom\n cueFromId\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextHrefParser\n popularity 0.000217\n cue href\n description Set HTML href attribute.\n extends abstractAftertextAttributeParser\n atoms cueAtom urlAtom\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nimportToFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nabstractColumnNameOrColorParser\n extends abstractColumnNameParser\n atoms cueAtom columnNameOrColorAtom\n javascript\n //todo: cleanup\n colorOptions = \"aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen\".split(\" \")\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameOrColorAtom\")\n return this.parent.columnNames.join(this.colorOptions)\n return super.getRunTimeEnumOptions(atom)\n }\nscrollFillParser\n cue fill\n extends abstractColumnNameOrColorParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameOrColorParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"166.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|toFooter$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"toFooter\")) {\n const pushes = particle.getParticles(\"toFooter\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
Breck Yunits
Breck Yunits
10 days ago
ScrollHub.js
Changed around line 1775: scrollVersionLink`
- certCache.set(hostname, sslOptions) // Cache the cert and key
+ this.certCache.set(hostname, sslOptions) // Cache the cert and key
Breck Yunits
Breck Yunits
10 days ago
ScrollHub.js
Changed around line 1799: scrollVersionLink`
- const crtInHubFolder = await fsp.readdir(hubFolder).find(f => f.endsWith(".crt"))
+ const crtInHubFolder = (await fsp.readdir(hubFolder)).find(f => f.endsWith(".crt"))
Breck Yunits
Breck Yunits
10 days ago
ssl refactor
Agents.js
Changed around line 407: class DeepSeek extends AbstractAgent {
- constructor(hubFolder) {
- this.hubFolder = hubFolder
+ constructor(hub) {
+ this.hubFolder = hub.hubFolder
+ this.config = hub.config
- const keyPath = path.join(hubFolder, `keys.txt`)
- this.keyFile = fs.existsSync(keyPath) ? Particle.fromDisk(keyPath) : new Particle()
Changed around line 418: class Agents {
- const apiKey = this.keyFile.get(name)
+ const apiKey = this.config.get(name)
CertificateMaker.js
Changed around line 117: class CertificateMaker {
- fs.writeFileSync(path.join(directory, `${domain}.crt`), certificate)
- fs.writeFileSync(path.join(directory, `${domain}.key`), domainPrivateKey)
+ fs.writeFileSync(path.join(directory, `.${domain}.crt`), certificate)
+ fs.writeFileSync(path.join(directory, `.${domain}.key`), domainPrivateKey)
ScrollHub.js
Changed around line 196: class ScrollHub {
- this.certsFolder = path.join(hubFolder, "certs")
Changed around line 204: class ScrollHub {
+ const configPath = path.join(hubFolder, `config.scroll`)
+ this.config = fs.existsSync(configPath) ? Particle.fromDisk(configPath) : new Particle()
Changed around line 723: If you'd like to create this folder, visit our main site to get started.
- const agents = new Agents(this.hubFolder)
+ const agents = new Agents(this)
Changed around line 762: If you'd like to create this folder, visit our main site to get started.
- const certPath = path.join(this.certsFolder, `${folderName}.crt`)
+ const certPath = this.makeCertPath(folderName)
+ makeCertPath(folderName) {
+ return path.join(this.rootFolder, folderName, `.${folderName}.crt`)
+ }
+
Changed around line 1552: If you'd like to create this folder, visit our main site to get started.
- const { hubFolder, rootFolder, trashFolder, certsFolder, publicFolder } = this
+ const { hubFolder, rootFolder, trashFolder, publicFolder } = this
- if (!fs.existsSync(certsFolder)) fs.mkdirSync(certsFolder)
Changed around line 1766: scrollVersionLink`
- loadCertAndKey(hostname) {
- const { certCache, pendingCerts, certsFolder } = this
- if (certCache.has(hostname)) return certCache.get(hostname) // Return from cache if available
-
- const certPath = path.join(certsFolder, `${hostname}.crt`)
- const keyPath = path.join(certsFolder, `${hostname}.key`)
+ loadCert(certPath, hostname) {
+ const keyPath = certPath.replace(/\.crt$/, ".key")
Changed around line 1777: scrollVersionLink`
- } else {
- if (pendingCerts[hostname]) return
- this.makeCert(hostname)
- throw new Error(`SSL certificate or key not found for ${hostname}. Attempting to make cert.`)
+ return false
+ }
+
+ loadCertAndKey(hostname) {
+ const { certCache, pendingCerts } = this
+ if (certCache.has(hostname)) return certCache.get(hostname) // Return from cache if available
+
+ const loadedCert = this.loadCert(this.makeCertPath(hostname), hostname)
+ if (loadedCert) return loadedCert
+ if (pendingCerts[hostname]) return
+ this.makeCert(hostname)
+ throw new Error(`SSL certificate or key not found for ${hostname}. Attempting to make cert.`)
- const { app, certsFolder } = this
+ const { app, hubFolder } = this
+ const crtInHubFolder = await fsp.readdir(hubFolder).find(f => f.endsWith(".crt"))
+ if (crtInHubFolder) {
+ const hostname = crtInHubFolder.substr(1).replace(/\.crt$/, "")
+ this.loadCert(path.join(hubFolder, crtInHubFolder), hostname)
+ }
+
- await certMaker.makeCertificate(domain, email, certsFolder)
+ await certMaker.makeCertificate(domain, email, path.join(this.rootFolder, folderName))
package.json
Changed around line 1
- "version": "0.76.0",
+ "version": "0.77.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.77.0 1/12/2025
+ 🎉 eliminated certs folder. Now store certs in their respective folders.
+ ⚠️ BREAKING: .hub/keys.txt is now .hub/config.scroll
+ ⚠️ BREAKING: put your root crt/key pair in the .hub folder for root SSL
+ ⚠️ BREAKING: use the migration script below, otherwise all certs will be regenerated.
+ codeWithHeader migrateCerts.sh
+ #!/bin/bash
+ cd /root/.hub/certs
+ # Iterate over all .crt files (we could use .key too, pattern will match pairs)
+ for cert in *.crt; do
+ # Skip if no matches found
+ [[ -f "$cert" ]] || continue
+ # Get the base name without extension (e.g., xiuzhen1.scroll.pub)
+ base="${cert%.crt}"
+ # Copy both cert and key with leading dot
+ cp "$cert" "/root/$base/.$cert"
+ cp "$base.key" "/root/$base/.$base.key"
+ done
+
Breck Yunits
Breck Yunits
10 days ago
claude is awesome
ScrollHub.js
Changed around line 208: class ScrollHub {
+ process.title = process.title + " - ScrollHubProcess"
cli.js
Changed around line 19: class ScrollHubCLI extends SimpleCLI {
+
+ getRunningScrollHubs() {
+ try {
+ // Get detailed process info including CPU and memory
+ const command = "ps aux | grep '[S]crollHubProcess' | awk '{printf \"%s\\t%s\\t%s\\t%s\\t%s\\t%s\\t%s %s %s\\n\", $2, $3, $4, $5, $6, $9, $11, $12, $13}'"
+ const output = child_process.execSync(command, { encoding: "utf-8" }).trim()
+
+ if (!output) return []
+
+ return output.split("\n").map(line => {
+ const [pid, cpu, mem, vsz, rss, startTime, command] = line.split("\t")
+ // Get listening ports for this process
+ const portsCommand = `lsof -Pan -p ${pid} -i | grep LISTEN | awk '{print $9}' | cut -d: -f2`
+ let ports = []
+ try {
+ ports = child_process.execSync(portsCommand, { encoding: "utf-8" }).trim().split("\n").filter(Boolean)
+ } catch (e) {
+ // Process might be gone or no permissions
+ }
+
+ return {
+ pid,
+ cpu: `${parseFloat(cpu).toFixed(1)}%`,
+ mem: `${parseFloat(mem).toFixed(1)}%`,
+ vsz: `${Math.round(vsz / 1024)}MB`, // Convert to MB
+ rss: `${Math.round(rss / 1024)}MB`, // Convert to MB
+ startTime,
+ ports,
+ command
+ }
+ })
+ } catch (error) {
+ console.error("Error getting ScrollHub processes:", error.message)
+ return []
+ }
+ }
+
+ listCommand(cwd) {
+ const processes = this.getRunningScrollHubs()
+
+ if (!processes.length) {
+ console.log("No ScrollHub processes currently running")
+ return
+ }
+
+ console.log("\nRunning ScrollHub Processes:")
+ console.log("PID\tCPU\tMEM\tRSS\tSTARTED\tPORTS\t\tCOMMAND")
+ console.log("-".repeat(80))
+
+ processes.forEach(proc => {
+ const ports = proc.ports.length ? proc.ports.join(", ") : "none"
+ console.log(`${proc.pid}\t` + `${proc.cpu}\t` + `${proc.mem}\t` + `${proc.rss}\t` + `${proc.startTime}\t` + `${ports}\t\t` + `${proc.command}`)
+ })
+ }
+
+ killCommand(cwd) {
+ const processes = this.getRunningScrollHubs()
+
+ if (!processes.length) {
+ console.log("No ScrollHub process found")
+ return
+ }
+
+ processes.forEach(proc => {
+ try {
+ child_process.execSync(`kill -9 ${proc.pid}`)
+ console.log(`Successfully killed ScrollHub process (PID: ${proc.pid})`)
+ } catch (error) {
+ console.error(`Error killing process ${proc.pid}:`, error.message)
+ }
+ })
+ }
package.json
Changed around line 1
- "version": "0.75.0",
+ "version": "0.76.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.76.0 1/12/2025
+ 🎉 add list and kill commands for easier sysadmin
+ 🎉 various improvements, new prompts, and fixes
+ 🏥 various fixes
+
Breck Yunits
Breck Yunits
12 days ago
public/scrollHubEditor.js
Changed around line 357: class EditorApp {
+ clearInterval(this.spinnerInterval)
Changed around line 413: class EditorApp {
+ return false
+ return false
+ return true
- await this.buildFolderCommand()
+ const result = await this.buildFolderCommand()
+ if (!result) return false
Breck Yunits
Breck Yunits
12 days ago
public/scrollHubEditor.js
Changed around line 411: class EditorApp {
- this.showError(message.message.split(".")[0])
+ this.showError(message.message?.split(".")[0] || "Error building folder.")
Breck Yunits
Breck Yunits
12 days ago
ScrollHub.js
Changed around line 740: If you'd like to create this folder, visit our main site to get started.
- await this.buildFolder(folderName)
+ try {
+ await this.buildFolder(folderName)
+ } catch (err) {
+ console.error(err)
+ }
Breck Yunits
Breck Yunits
13 days ago
package.json
Changed around line 19
- "scroll-cli": "^165.3.0",
+ "scroll-cli": "^165.4.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollSectionParser\n popularity 0.007524\n description HTML section tag.\n extends abstractIndentableParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\npParagraphParser\n popularity 0.001881\n cue p\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nscrollDivParser\n popularity 0.007524\n description HTML div tag.\n extends abstractIndentableParagraphParser\n cue div\n example\n div\n # Hello world\n p This is a test\n div\n # Deeper\n javascript\n tag = \"div\"\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\npParagraphParser\n popularity 0.001881\n cue p\n extends abstractIndentableParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollSectionParser\n popularity 0.007524\n description HTML section tag.\n extends abstractIndentableParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh1LiteralParser\n extends h1Parser\n cue h1\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh2LiteralParser\n extends h2Parser\n cue h2\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh3LiteralParser\n extends h3Parser\n cue h3\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.4.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
13 days ago
package.json
Changed around line 19
- "scroll-cli": "^165.2.0",
+ "scroll-cli": "^165.3.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.htmlContents}${this.closingTag}`\n }\n get htmlContents() {\n return this.text\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n get htmlContents() {\n return this.text + this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollSectionParser\n popularity 0.007524\n description HTML section tag.\n extends abstractIndentableParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser\n cue section\n example\n section\n # Hello world\n p This is a test\n section\n # Deeper\n javascript\n tag = \"section\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n example\n # Hello world\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n example\n ## Hello world\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n example\n ### Hello world\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n example\n #### Hello world\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n example\n ##### Hello world\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n example\n map.png\n caption A map.\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\n example\n sipOfCoffee.m4a\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n example\n spirit.mp4\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\npParagraphParser\n popularity 0.001881\n cue p\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n p I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n example\n container 600px\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n example\n ---\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n example\n ***\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n example\n ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n example\n \n assertHtmlEquals \n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n example\n buildCsv\n assertBuildIncludes virginica\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n example\n buildCsv\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\n example\n buildCss\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\n example\n baseUrl https://hub.scroll.pub/\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n example\n css\n body { color: red;}\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n example\n background red\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n example\n font Slim\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n example\n style.css\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n example\n body.html\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n example\n script.js\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n example\n belowAsCode\n iris\n printTable\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n example\n belowAsHtml\n # Hello world\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n example\n # Hello world\n aboveAsHtml\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n example\n

Inline HTML

\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n example\n br 2\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n example\n screenshot.png\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n get docs() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category, examples, cue} = obj\n const example = examples.length ? examples[0].subparticlesToString() : cue\n return {\n id,\n isPopular,\n description,\n popularity,\n example,\n category\n }\n })\n return this.root.lodash.sortBy(rows, \"isPopular\")\n }\n buildJson() {\n return JSON.stringify(this.docs, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.docs).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n example\n theme gazette\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\n example\n color blue\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
13 days ago
ScrollHub.js
Changed around line 456: If you'd like to create this folder, visit our main site to get started.
- const { app, rootFolder, hubFolder } = this
+ const { app, rootFolder, publicFolder } = this
Changed around line 469: If you'd like to create this folder, visit our main site to get started.
- res.status(404).sendFile(path.join(hubFolder, "404.html"))
+ res.status(404).sendFile(path.join(publicFolder, "404.html"))
Breck Yunits
Breck Yunits
13 days ago
Agents.js
Changed around line 108: script.js
+ class DatabasePrompt extends SimpleCreationPrompt {
+ get systemPrompt() {
+ return `You are an expert web, scroll, and knowledge base developer. Create a knowledge base using ScrollSets based on this request: "${this.userPrompt}"
+
+ ScrollSets use the Particles, Parsers, and Scroll stack to define measures, and then encode concepts using those measures.
+
+ For example:
+
+ ---measures.parsers---
+ idParser
+ extends abstractIdParser
+ organismParser
+ extends abstractStringMeasureParser
+ description The organism name mainly associated with the organelle such as human, plant, whale, etc.
+ diameterParser
+ extends abstractIntegerMeasureParser
+ description The diameter of the organelle in nanometers
+ lowParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found on the low end?
+ medianParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found in the median?
+ highParser
+ extends abstractIntegerMeasureParser
+ description For cells that have this kind of organelle, how many are usually found on the high end?
+
+ ---concepts.scroll---
+ measures.parsers
+
+ id Mitochondria
+ organism human
+ diameter 1000
+ low 200
+ median 500
+ high 2000
+
+ id Chloroplast
+ organism plant
+ diameter 6000
+ low 20
+ median 40
+ high 100
+
+ id Nucleus
+ organism human
+ diameter 6000
+ low 1
+ median 1
+ high 2
+
+ ---
+
+ Requirements:
+ - Create 5 - 7 measures. The most important things about this topic.
+ - Write 5 concepts. The most important concepts in this topic.
+ - Build a nice homepage iterating over the data.
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---measures.parsers---
+ (put measures here)
+ ---concepts.scroll---
+ (put concepts here)
+ ---index.scroll---
+ title (database title here)
+ header.scroll
+ (content here)
+ concepts.scroll
+ concepts
+ printTable
+ footer.scroll
+ ---header.scroll---
+ importOnly
+ buildHtml
+ homeButton
+ metaTags
+ editButton
+ editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ metaTags
+ style.css
+ script.js
+ ---footer.scroll---
+ importOnly
+ center
+ scrollVersionLink
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---`
+ }
+ }
+
Changed around line 437: class Agents {
- slideshow: SlideshowCreationPrompt
+ slideshow: SlideshowCreationPrompt,
+ database: DatabasePrompt
public/claudeDatabase.scroll
Changed around line 1
+ indexTop.scroll
+ replace AGENT Claude
+ replace TEMPLATE database
+ createFromPrompt.scroll
+ indexBottom.scroll
public/createLinks.scroll
Changed around line 1
- Website   Blog   Slideshow   Blank
+ Website   Blog   Slideshow   Database   Blank
+ claudeDatabase.html Database
Breck Yunits
Breck Yunits
13 days ago
Agents.js
Changed around line 191: Requirements:
- Do not put a copyright symbol or all rights reserved or confidential or any of that mumbo jumbo.
- Make it beautiful. Dazzling. Advanced used of CSS.
+ Below is the Scroll Parser that implements the Slideshow. This should give you the class names for the styles.
+ slideshowParser
+ // Left and right arrows navigate.
+ description Slideshow widget. *** delimits slides.
+ extends abstractScrollWithRequirementsParser
+ string copyFromExternal .jquery-3.7.1.min.js .slideshow.js
+ example
+ slideshow
+ Why did the cow cross the road?
+ ***
+ Because it wanted to go to the MOOOO-vies.
+ ***
+ THE END
+ ****
+ javascript
+ buildHtml() {
+ return \`
\`
+ }
+
package.json
Changed around line 19
- "scroll-cli": "^165.0.0",
+ "scroll-cli": "^165.2.0",
public/createFromPrompt.scroll
Changed around line 35: css
- color: unset;
+ color: #1a2b4d;
public/createLinks.scroll
Changed around line 1
- Claude   DeepSeek   Blank
- claude.html Claude
- deepseek.html DeepSeek
+ Website   Blog   Slideshow   Blank
+ claude.html Website
+ deepseekBlog.html Blog
+ claudeSlideshow.html Slideshow
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"164.12.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${root.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Description for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\nopenGraphKeywordsParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue keywords\n description Keywords for SEO meta tag.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const keywordsTag = keywords ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n ${keywordsTag}\n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return this.getInBrowser(fileOrUrl)\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n getInBrowser(key) {\n return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : \"\") || \"\"\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = this.getInBrowser(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n const text = await blob.text()\n try {\n localStorage.setItem(url, text)\n return localStorage.getItem(url)\n } catch (err) {\n if (!window.inMemStorage) window.inMemStorage = {}\n window.inMemStorage[url] = text\n console.error(err)\n return text\n }\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"165.2.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get keywords() {\n if (this.has(\"keywords\"))\n return this.get(\"keywords\")\n const tags = this.get(\"tags\")\n if (tags)\n return tags.split(\" \").join(\", \")\n return \"\"\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
13 days ago
Agents.js
Changed around line 19: class AbstractPrompt {
- }
-
- class SimpleCreationPrompt extends AbstractPrompt {
- get systemPrompt() {
- return `You are an expert web developer. Create a website based on this request: "${this.userPrompt}"
-
- Requirements:
- - Use only Scroll, vanilla HTML, CSS, and JavaScript (NO frameworks, NO external dependencies)
- - Create clean, semantic HTML5
- - Make it mobile-responsive
- - Follow modern best practices and accessibility guidelines
- - Keep it simple but professional
- - Include basic SEO meta tags using Scroll
- - Use only relative links and no external resources
- - Do not put a copyright symbol or all rights reserved in the footer.
- - Make it beautiful. Dazzling. Advanced used of CSS.
-
- First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
-
- (domain.scroll.pub here)
- buildHtml
- baseUrl https://(domain.scroll.pub here)
- metaTags
- editButton /edit.html
- title (Title here)
- style.css
- body.html
- script.js
- (HTML body content here)
- (CSS content here)
- (JavaScript content here)
- }
Changed around line 70: script.js
+ class SimpleCreationPrompt extends AbstractPrompt {
+ get systemPrompt() {
+ return `You are an expert web developer. Create a website based on this request: "${this.userPrompt}"
+
+ Requirements:
+ - Use only Scroll, vanilla HTML, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Include basic SEO meta tags using Scroll
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ buildHtml
+ baseUrl https://(domain.scroll.pub here)
+ metaTags
+ editButton /edit.html
+ title (Title here)
+ style.css
+ body.html
+ script.js
+ ---body.html---
+ (HTML body content here)
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---`
+ }
+ }
+
Changed around line 175: scrollVersionLink
+ class SlideshowCreationPrompt extends SimpleCreationPrompt {
+ get systemPrompt() {
+ return `You are an expert design agency tasked with helping someone create a slideshow. Create a slideshow based on this request: "${this.userPrompt}"
+
+ Requirements:
+ - Make the slideshow inspirational, concise, witty.
+ - The scroll slideshow keyword automatically injects javascript to handle the showing/hiding of slide content, navigation, urls, etc. You just need to add content and style.
+ - Use mostly Scroll, but you can also use CSS, HTML and JavaScript (NO frameworks, NO external dependencies)
+ - Make it mobile-responsive
+ - 5-10 slides
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved or confidential or any of that mumbo jumbo.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this slideshow. Then provide the files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ buildTxt
+ buildHtml
+ title (slideshow title for meta tags)
+ metaTags
+ style.css
+ script.js
+ slideshow
+ ***
+
+ (first slide)
+
+ ***
+
+ (slides here, delimited by ***)
+
+ ***
+
+ (Final slide)
+
+ ****
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (Any JavaScript content here, if needed)
+ ---end---`
+ }
+ }
+
Changed around line 313: class Agents {
- blog: BlogCreationPrompt
+ blog: BlogCreationPrompt,
+ slideshow: SlideshowCreationPrompt
public/claudeSlideshow.scroll
Changed around line 1
+ indexTop.scroll
+ replace AGENT Claude
+ replace TEMPLATE slideshow
+ createFromPrompt.scroll
+ indexBottom.scroll
public/deepseekSlideshow.scroll
Changed around line 1
+ indexTop.scroll
+ replace AGENT DeepSeek
+ replace TEMPLATE slideshow
+ createFromPrompt.scroll
+ indexBottom.scroll
Breck Yunits
Breck Yunits
14 days ago
Add blog prompt
Agents.js
Changed around line 5: const OpenAI = require("openai")
- constructor(userPrompt, existingNames, agent) {
+ constructor(userPrompt, existingNames, agent, what) {
+ this.what = what
Changed around line 99: script.js
- files["readme.scroll"] = `# ${finalDomain}\nWebsite generated by ${this.agent.name} from prompt: ${this.userPrompt}`
+ files["readme.scroll"] = `# ${finalDomain}\n\n${this.what} generated by ${this.agent.name} from prompt: ${this.userPrompt}`
Changed around line 108: script.js
+ class BlogCreationPrompt extends SimpleCreationPrompt {
+ get systemPrompt() {
+ return `You are an expert web developer. Create a blog based on this request: "${this.userPrompt}"
+
+ Requirements:
+ - Write 2 blog posts. Keep them short. Intelligent. Data backed. Witty.
+ - Use only Scroll, CSS, and JavaScript (NO frameworks, NO external dependencies)
+ - Create clean, semantic HTML5
+ - Make it mobile-responsive
+ - Follow modern best practices and accessibility guidelines
+ - Keep it simple but professional
+ - Include basic SEO meta tags using Scroll
+ - Use only relative links and no external resources
+ - Do not put a copyright symbol or all rights reserved in the footer.
+ - Make it beautiful. Dazzling. Advanced used of CSS.
+
+ First suggest a short, memorable domain name ending in .scroll.pub that represents this website. Then provide the website files. Use this exact format:
+
+ ---domain---
+ (domain.scroll.pub here)
+ ---index.scroll---
+ title (blog title here)
+ header.scroll
+ printSnippets All
+ footer.scroll
+ ---(firstPostPermalinkHere).scroll---
+ title (first post title here)
+ tags All
+ header.scroll
+ (first post content here)
+ footer.scroll
+ ---(secondPostPermalinkHere).scroll---
+ title (second post title here)
+ tags All
+ header.scroll
+ (second post content here)
+ footer.scroll
+ ---header.scroll---
+ importOnly
+ buildHtml
+ buildTxt
+ homeButton
+ leftRightButtons
+ baseUrl https://(domain.scroll.pub here)
+ metaTags
+ editButton
+ editBaseUrl /edit.html?folderName=(domain.scroll.pub here)&fileName=
+ metaTags
+ style.css
+ script.js
+ container
+ printTitle
+ ---feed.scroll---
+ buildRss
+ printFeed All
+ ---footer.scroll---
+ importOnly
+ center
+ scrollVersionLink
+ ---style.css---
+ (CSS content here)
+ ---script.js---
+ (JavaScript content here)
+ ---end---`
+ }
+ }
+
Changed around line 262: class Agents {
- async createFolderNameAndFilesFromPrompt(userPrompt, existingNames, agentName) {
+ availablePrompts = {
+ website: SimpleCreationPrompt,
+ blog: BlogCreationPrompt
+ }
+
+ async createFolderNameAndFilesFromPrompt(userPrompt, existingNames, agentName, promptTemplate = "website") {
- const prompt = new SimpleCreationPrompt(userPrompt, existingNames, agent)
+ const promptToUse = this.availablePrompts[promptTemplate]
+ const prompt = new promptToUse(userPrompt, existingNames, agent, promptTemplate)
ScrollHub.js
Changed around line 726: If you'd like to create this folder, visit our main site to get started.
+ const template = req.body.template
- const response = await agents.createFolderNameAndFilesFromPrompt(prompt, existingNames, agent)
+ const response = await agents.createFolderNameAndFilesFromPrompt(prompt, existingNames, agent, template)
public/claude.scroll
Changed around line 1
+ replace TEMPLATE website
public/claudeBlog.scroll
Changed around line 1
+ indexTop.scroll
+ replace AGENT Claude
+ replace TEMPLATE blog
+ createFromPrompt.scroll
+ indexBottom.scroll
public/createFromPrompt.scroll
Changed around line 1
-
+
+
Changed around line 10: inlineJs create.js
-

Creating your website...

+

Creating your TEMPLATE...

-

While you wait, say hi via breck@scroll.pub or X

+

While you wait, say hi on X

public/deepseek.scroll
Changed around line 1
+ replace TEMPLATE website
public/deepseekBlog.scroll
Changed around line 1
+ indexTop.scroll
+ replace AGENT DeepSeek
+ replace TEMPLATE blog
+ createFromPrompt.scroll
+ indexBottom.scroll
Breck Yunits
Breck Yunits
14 days ago
Agents.js
Changed around line 2: const { Anthropic } = require("@anthropic-ai/sdk")
+ const { Particle } = require("scrollsdk/products/Particle.js")
Changed around line 170: class Agents {
+ const keyPath = path.join(hubFolder, `keys.txt`)
+ this.keyFile = fs.existsSync(keyPath) ? Particle.fromDisk(keyPath) : new Particle()
Changed around line 179: class Agents {
- const keyPath = path.join(hubFolder, `${name}.txt`)
- if (!fs.existsSync(keyPath)) {
+ const apiKey = this.keyFile.get(name)
+ if (!apiKey) {
+ } else {
+ console.log(`${name} agent loaded.`)
- const apiKey = fs.readFileSync(keyPath, "utf8").trim()
Breck Yunits
Breck Yunits
14 days ago
package.json
Changed around line 1
- "version": "0.74.0",
+ "version": "0.75.0",
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.75.0 1/7/2025
+ 🎉 show file history command
+ 🏥 fix rehighlighting bug with remote imports
+
Breck Yunits
Breck Yunits
14 days ago
Fix highlighting with remote imports
public/scrollHubEditor.js
Changed around line 159: class EditorApp {
- console.log("rehighlighting needed")
+ console.log("rehighlighting needed - reinitializing CodeMirror")
- // todo: figure this out
+ // Store current state
+ const currentContent = this.codeMirrorInstance.getValue()
+ const currentCursor = this.codeMirrorInstance.getCursor()
+ const currentScrollInfo = this.codeMirrorInstance.getScrollInfo()
+
+ // Completely reinitialize CodeMirror with current mode
+ this.initCodeMirror(this.mode)
+
+ // Restore content and cursor position
+ this.codeMirrorInstance.setValue(currentContent)
+ this.codeMirrorInstance.setCursor(currentCursor)
+
+ // Restore scroll position
+ this.codeMirrorInstance.scrollTo(currentScrollInfo.left, currentScrollInfo.top)
- updateEditorMode(fileName) {
- const mode = this.getEditorMode(fileName)
+ updateEditorMode(mode) {
Changed around line 252: class EditorApp {
+ await this.refreshParserCommand()
Changed around line 315: class EditorApp {
- this.updateEditorMode(fileName)
+ this.updateEditorMode(this.getEditorMode(fileName))
Breck Yunits
Breck Yunits
15 days ago
Add show file history command
ScrollHub.js
Changed around line 1203: If you'd like to create this folder, visit our main site to get started.
+
+ app.get("/blame.htm", checkWritePermissions, async (req, res) => {
+ const fileName = req.query.fileName.replace(/[^a-zA-Z0-9\/_.-]/g, "")
+ await this.runCommand(req, res, "git blame " + fileName)
+ })
public/scrollHubEditor.js
Changed around line 561: class EditorApp {
+ command+h showFileHistoryCommand File
Changed around line 681: Follow me on X or GitHub
+ async showFileHistoryCommand(event) {
+ if (!this.fileName) return
+
+ try {
+ this.showSpinner("Loading file history...")
+ const response = await fetch(`/blame.htm?folderName=${this.folderName}&fileName=${encodeURIComponent(this.fileName)}`)
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch file history")
+ }
+
+ const history = await response.text()
+
+ // Format the history data into HTML
+ const formattedHistory = `
+
+

File History: ${this.fileName}

+
${history}
+
+ `
+
+ this.openModal(formattedHistory, "history", event)
+ this.hideSpinner()
+ } catch (error) {
+ console.error("Error fetching file history:", error)
+ this.showError("Failed to load file history: " + error.message)
+ setTimeout(() => this.hideSpinner(), 3000)
+ }
+ }
+
Breck Yunits
Breck Yunits
15 days ago
public/scrollHubEditor.js
Changed around line 336: class EditorApp {
+ showSpinnerWithStopwatch(message, style) {
+ this.showSpinner(message, style)
+ let count = 1
+ this.spinnerInterval = setInterval(() => (document.querySelector("#spinner").innerHTML = `${message} - ${count++}s`), 1000)
+ document.querySelector("#spinner").style.display = "block"
+ }
+
+ clearInterval(this.spinnerInterval)
Changed around line 408: class EditorApp {
+ this.showSpinnerWithStopwatch("Building")
+ this.hideSpinner()
Breck Yunits
Breck Yunits
15 days ago
package.json
Changed around line 1
- "version": "0.73.0",
+ "version": "0.74.0",
public/FusionEditor.js
Changed around line 40: class FusionEditor {
- async getFusedFile() {
- const { bufferValue, ScrollFile } = this
- const filename = "/" + this.parent.fileName
- this.fakeFs[filename] = bufferValue
- delete this.fs._expandedImportCache[filename] // todo: cleanup
- const file = new ScrollFile(bufferValue, filename, this.fs)
+ async makeFusedFile(code, filename) {
+ const { ScrollFile, fs } = this
+ this.fakeFs[filename] = code
+ delete fs._expandedImportCache[filename] // todo: cleanup
+ const file = new ScrollFile(code, filename, fs)
+ return file
+ }
+ async getFusedFile() {
+ const file = await this.makeFusedFile(this.bufferValue, "/" + this.parent.fileName)
Changed around line 73: class FusionEditor {
- await this._mainProgram.load()
+ try {
+ await this._mainProgram.load()
+ } catch (err) {
+ console.error(err)
+ }
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.74.0 1/7/2025
+ 🎉 various improvements and fixes
+ 🏥 continue gracefully if errors in loading a scroll doc
+
public/scrollHubEditor.js
Changed around line 237: class EditorApp {
- this.fetchAndDisplayFileList()
+ this.refreshFileListCommand()
- await this.fetchAndDisplayFileList()
+ await this.refreshFileListCommand()
- this.fetchAndDisplayFileList()
+ this.refreshFileListCommand()
Changed around line 305: class EditorApp {
- if (!this.files) await this.fetchAndDisplayFileList()
+ if (!this.files) await this.refreshFileListCommand()
Changed around line 370: class EditorApp {
- await this.fetchAndDisplayFileList()
+ await this.refreshFileListCommand()
Changed around line 399: class EditorApp {
+ async buildFolderAndRefreshCommand() {
+ await this.buildFolderCommand()
+ await this.refreshFileListCommand()
+ }
+
- await this.fetchAndDisplayFileList()
+ await this.refreshFileListCommand()
Changed around line 503: class EditorApp {
-
-
Changed around line 513: class EditorApp {
+
+
Changed around line 551: class EditorApp {
+ command+b buildFolderAndRefreshCommand Folder
Changed around line 714: Follow me on X or GitHub
- this.fetchAndDisplayFileList()
+ this.refreshFileListCommand()
Changed around line 875: Follow me on X or GitHub
- async fetchAndDisplayFileList() {
+ async refreshFileListCommand() {
Changed around line 971: Follow me on X or GitHub
- this.fetchAndDisplayFileList()
+ this.refreshFileListCommand()
Changed around line 1045: Follow me on X or GitHub
- await this.fetchAndDisplayFileList()
+ await this.refreshFileListCommand()
- await this.fetchAndDisplayFileList()
+ await this.refreshFileListCommand()
public/scrollHubStyle.css
Changed around line 299: textarea {
+ .shortcut-category {
+ break-inside: avoid;
+ }
+
- display: grid;
- gap: 12px;
+ column-count: auto;
+ column-fill: auto;
+ column-width: 40ch;
+ column-gap: 1rem;
+ height: calc(100vh - 300px);
- display: flex;
- align-items: center;
- gap: 12px;
+ padding: 8px 0;
- }
-
- .shortcut:hover kbd {
- background: var(--kbd-hover);
+ display: block;
Changed around line 329: textarea {
+ .shortcut:hover kbd {
+ background: var(--kbd-hover);
+ }
+
Breck Yunits
Breck Yunits
15 days ago
no spaces in filenames
public/edit.scroll
public/scrollHubEditor.js
Changed around line 218: class EditorApp {
- this.bindFileButtons()
Changed around line 347: class EditorApp {
- let fileName = prompt("Enter a filename", name)
+ let fileName = this.sanitizeFileName(prompt("Enter a filename", name))
- this.fileName = this.sanitizeFileName(fileName)
+ this.fileName = fileName
Changed around line 358: class EditorApp {
- fileName = this.sanitizeFileName(fileName)
Changed around line 977: Follow me on X or GitHub
- return fileName.includes(".") ? fileName : fileName + ".scroll"
+ if (fileName === undefined || fileName === null) return ""
+ // Dont allow spaces in filenames. And if no extension, auto add .scroll.
+ return (fileName.includes(".") ? fileName : fileName + ".scroll").replace(/ /g, "")
- let fileName = prompt("Enter a filename", "untitled").replace(/ /g, "")
+ let fileName = this.sanitizeFileName(prompt("Enter a filename", "untitled"))
Changed around line 1005: Follow me on X or GitHub
- async duplicateFile() {
+ async duplicateFileCommand() {
- const newFileName = prompt(`Enter name for the duplicate of "${fileName}":`, defaultNewName)
+ const newFileName = this.sanitizeFileName(prompt(`Enter name for the duplicate of "${fileName}":`, defaultNewName))
- await this.writeFile("Duplicating...", this.bufferValue, this.sanitizeFileName(newFileName))
+ await this.writeFile("Duplicating...", this.bufferValue, newFileName)
Changed around line 1022: Follow me on X or GitHub
- bindFileButtons() {
- const that = this
- document.querySelector(".renameFileLink").addEventListener("click", async e => {
- const oldFileName = that.fileName
- const newFileName = prompt(`Enter new name for "${oldFileName}":`, oldFileName)
- if (newFileName && newFileName !== oldFileName) {
- this.performFileRename(oldFileName, this.sanitizeFileName(newFileName))
- }
- })
+ async renameFileCommand() {
+ const oldFileName = this.fileName
+ const newFileName = this.sanitizeFileName(prompt(`Enter new name for "${oldFileName}":`, oldFileName))
+ if (newFileName && newFileName !== oldFileName) this.performFileRename(oldFileName, newFileName)
+ }
- document.querySelector(".duplicateFileLink").addEventListener("click", async evt => {
- evt.preventDefault()
- this.duplicateFile()
- })
+ async deleteFileCommand() {
+ const { fileName, folderName } = this
+ const userInput = prompt(`To delete this file, please type the file name: ${fileName}`)
- document.querySelector(".formatFileLink").addEventListener("click", async evt => {
- evt.preventDefault()
- this.formatFileCommand()
+ if (!userInput || userInput !== fileName) return
+ this.showSpinner("Deleting...")
+ const response = await fetch(`/deleteFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(fileName)}`, {
+ method: "POST"
- document.querySelector(".deleteFileLink").addEventListener("click", async e => {
- e.preventDefault()
-
- const { fileName, folderName } = this
- const userInput = prompt(`To delete this file, please type the file name: ${fileName}`)
-
- if (!userInput || userInput !== fileName) return
- this.showSpinner("Deleting...")
- const response = await fetch(`/deleteFile.htm?folderName=${folderName}&filePath=${encodeURIComponent(fileName)}`, {
- method: "POST"
- })
-
- const data = await response.text()
- await this.fetchAndDisplayFileList()
- this.autoOpen()
- this.hideSpinner()
- })
+ const data = await response.text()
+ await this.fetchAndDisplayFileList()
+ await this.autoOpen()
+ await this.buildFolderCommand()
+ await this.fetchAndDisplayFileList()
+ this.hideSpinner()
Breck Yunits
Breck Yunits
15 days ago
public/FusionEditor.js
Changed around line 31: class FusionEditor {
+ const parsed = await this.parseScroll(scrollCode)
+ return parsed.asHtml
+ }
+ async parseScroll(scrollCode) {
- return page.scrollProgram.asHtml
+ return page.scrollProgram
Breck Yunits
Breck Yunits
16 days ago
package.json
Changed around line 19
- "scroll-cli": "^164.12.0",
+ "scroll-cli": "^165.0.0",
public/ide/parsers.js
Changed around line 1
- const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"164.12.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
+ const AppConstants = {"parsers":"columnNameAtom\n paint constant\nnewColumnNameAtom\n description Name a derived column.\n paint variable\nconstantAtom\n paint constant\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\n paint constant.language\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n paint constant.language\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint constant\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n paint constant\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\njavascriptAnyAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ncolorAtom\n extends codeAtom\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nreductionTypeAtom\n enum sum mean max min concat first\n paint keyword\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint constant\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\nvegaDataSetAtom\n paint constant.numeric\n enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\" {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `${note.label}`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return ``\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `${super.buildHtml()}\n `\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"scrollLinkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `
`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return ``\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* 0 ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"\")\n return `
${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `${this.label} ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"
\")\n return `
` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return ``\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* 0.0 `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"
\") // Single columns are self-closing after section break.\n return stackContents + `
`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"
\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"
\")\n return `` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\ndebugSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `${this.buildTxt().replace(/\\`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `
${this.dinkus}
`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `
`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `${this.svg}`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg \n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg \n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg \n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg \n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg \n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return ``\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n \n string style\n \n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = ``\n else\n tag = ``\n return `
${tag}
`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}
${this.inputs}${this.footer}
`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n \n \n \n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `\n \n `\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nscrollModalParser\n description A modal dialog overlay.\n extends abstractAftertextParser\n boolean isHtml true\n cue modal\n string requireOnce\n \n \n javascript\n buildHtml(buildSettings) {\n this.parent.sectionStack.push(\"
\")\n return this.getHtmlRequirements(buildSettings) + `
`\n }\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `Continue reading...`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `
${this.makeSnippet(file.file.scrollProgram, buildSettings)}
`\n }).join(\"\\n\\n\")\n return `
${snippets}
`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return ``\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `
${title}
${description}...
${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}
`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `
`\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n catchAllAtomType codeAtom\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual, expected} = this\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n get expected() {\n return this.length ? this.subparticlesToString() : (this.content ? this.content : \"\")\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertIgnoreBelowErrorsParser\n description If you want to ignore any errors in the below particle in automated tests.\n extends abstractScrollParser\n string bindTo next\n cueFromId\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType stringAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `
${this.day}
`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension, options) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne(options) { await this._buildFileType(this.extension, options) }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildOneCommandParser\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n await this._buildFileType(this.extension, options)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(options) {\n await this._copyExternalFiles(options)\n await super.buildTwo(options)\n }\n async _copyExternalFiles(options) {\n if (!this.isNodeJs()) return\n const {root} = this\n const externalFilesCopied = options.externalFilesCopied || {}\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom columnNameAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo(options) {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractInlineFileParser\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\n string joinChar ;\\n\\n\n string htmlTag script\n javascript\n get files() {\n const inline = this.atoms.slice(1)\n const children = this.map(particle => particle.cue)\n return inline.concat(children)\n }\n get contents() {\n return this.files.map(filename => this.root.readFile(filename)).join(this.joinChar)\n }\n buildHtml() {\n return `<${this.htmlTag}>/* ${this.files.join(\" \")} */\\n${this.contents}`\n }\nscrollInlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineCss\n string joinChar \\n\\n\n string htmlTag style\n javascript\n buildCss() {\n return this.contents\n }\nscrollInlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractInlineFileParser\n cue inlineJs\n javascript\n buildJs() {\n return this.contents\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType stringAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `
${line.asString}
` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split('?')[0].split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = this.root.d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nvegaSampleDataParser\n extends scrollTableParser\n description Sample dataset from Vega.\n cue sampleData\n atoms cueAtom vegaDataSetAtom\n example\n sampleData zipcodes.csv\n printTable\n javascript\n get filename() {\n return \"https://ohayo.scroll.pub/ohayo/packages/vega/datasets/\" + this.content\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/ file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `\n \n \n ${title}\n ${baseUrl}\n ${description}\n ${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n en-us\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n \n `\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `
${this.content}
${super.buildHtml()}
`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\ndebugParsersParser\n description Print the parsers used.\n extends codeParser\n cueFromId\n javascript\n buildParsers() { return this.code}\n get code() {\n let code = new Particle(this.root.definition.toString())\n // Remove comments\n code.filter((line) => line.getLine().startsWith(\"//\")).forEach((particle) => particle.destroy())\n // Remove blank lines\n code = code.toString().replace(/^\\n/gm, \"\")\n return code\n }\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n \nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = ``\n const firstRow = this.table[0]\n return (\n `
${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `
${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return ``\n })\n .join(\"\")}
`\n })\n .join(\"\\n\") +\n \"
\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n colorParser\n atoms cueAtom colorAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType colorAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `
\n `\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n \n \n example\n plot\n inScope abstractColumnNameParser\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n titleParser\n extends abstractPlotLabelParser\n subtitleParser\n extends abstractPlotLabelParser\n captionParser\n extends abstractPlotLabelParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `
`\n }\n get sortExpression() {\n const sort = this.get(\"sort\")\n if (!sort) return \"\"\n let sort_expr = \"\"\n if (sort.startsWith(\"-\")) {\n // Sort by a value descending\n const sortCol = sort.slice(1)\n sort_expr = `, sort: {x: \"y\", reverse: true}`\n } else if (sort.includes(\" \")) {\n // Fixed order specified\n const order = sort.split(\" \")\n sort_expr = `, sort: {x: (a,b) => {\n const order = ${JSON.stringify(order)};\n return order.indexOf(a) - order.indexOf(b)\n }}`\n } else if (sort === \"asc\") {\n sort_expr = `, sort: {x: \"x\"}`\n } else if (sort === \"desc\") {\n sort_expr = `, sort: {x: \"x\", reverse: true}`\n }\n return sort_expr\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\") || this.has(\"stroke\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n width: ${this.get(\"width\") || 640},\n height: ${this.get(\"height\") || 400},\n }`\n }\nplotScatterplotParser\n cue scatterplot\n extends abstractPlotParser\n description Scatterplot Widget.\n example\n iris\n scatterplot\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true${this.sortExpression},\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nplotBarchartParser\n cue barchart\n extends abstractPlotParser\n description Bar chart widget.\n example\n iris\n barchart\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n const fill = this.get(\"fill\")\n return `Plot.barY(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n fill: get(\"${fill}\"),\n tip: true${this.sortExpression}\n }), Plot.ruleY([0])`\n }\nplotLineChartParser\n cue linechart\n extends abstractPlotParser\n description Line chart widget.\n example\n iris\n linechart\n x SepalLength\n y SepalWidth\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const stroke = this.get(\"stroke\") || \"steelblue\"\n const strokeWidth = this.get(\"strokeWidth\") || 2\n const strokeLinecap = this.get(\"strokeLinecap\") || \"round\"\n const fill = this.get(\"fill\")\n return `Plot.line(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n stroke: \"${stroke}\",\n fill: get(\"${fill}\"),\n strokeWidth: ${strokeWidth},\n strokeLinecap: \"${strokeLinecap}\"${this.sortExpression}\n })`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce \n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return ``\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `${column.name}\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `${text}`\n else if (col.name.endsWith(\"Url\")) tagged = `${col.name.replace(\"Url\", \"\")}`\n else if (isUrl) tagged = `${text}`\n str += `${tagged}\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `${this.toRow(row)}`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `\n ${this.tableHeader.join(\"\\n\")}\n ${this.tableBody}\n
`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n \n \n \n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `
${content + this.subparticlesToString()}
`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `

`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `
`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n \n \n \n \n \n \n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCss() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return ``\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return ``\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return ``\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `` + items.map(particle => `${particle.cue}${particle.content}`).join(\"\\n\") + `\\n`\n }\n buildHtml() {\n return `${this.tableBody}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = thisethod]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = nextethod]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `${this.code.replace(/\\`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\ndebugBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .debug.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `
${particle.constructor.name}${particle.atoms.map((atom, index) => `${atom}${atomTypes[index]}`).join(\" \")}${(particle.length ? `
` + particle.map(mapFn).join(\"
\") + `
` : \"\")}
`}\n return this.selectedParticles.map(mapFn).join(\"
\")\n }\n buildHtml() {\n return `` + this.code\n }\n buildTxt() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `${particle.constructor.name} ${particle.atoms.map((atom, index) => `${atomTypes[index]}:${atom}`).join(\" \")}${(particle.length ? `\\n ` + particle.map(mapFn).join(\"\\n\") + `` : \"\")}`}\n return this.selectedParticles.map(mapFn).join(\"\\n\")\n }\n buildParsers() {return this.buildTxt()}\ndebugAboveParser\n description Inspect particle above.\n extends debugBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\ndebugAllParser\n description Inspect entire document.\n extends debugBelowParser\n javascript\n get selectedParticles() { return this.root.getSubparticles()}\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType codeAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\nbelowAsHtmlParser\n extends belowAsCodeParser\n description Displays html output of next particle in a code block.\n cueFromId\n javascript\n get code() {\n return this.selectedParticles.filter(p => p.buildHtml).map(p => p.buildHtml()).join(\"\\n\")\n }\naboveAsHtmlParser\n description Displays html output of previous particle in a code block.\n extends belowAsHtmlParser\n javascript\n method = \"previous\"\n reverse = true\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return ``\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCss() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(``)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `
`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => ``).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `
${caption.buildHtml()}
` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `
${this.getFigureContent(buildSettings)}${captionFig}
`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser scrollLinkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"scrollLinkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return ``\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `
`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType javascriptAnyAtom\n javascript\n buildHtml() {\n return ``\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return ``\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = ``\n return `
${linkToPrevious} · ${root.permalink} · ${linkToNext}${script}
`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `
` + doc.map(obj => `
${obj.isPopular ? \"\" : \"\"}${obj.id} ${obj.description}${obj.isPopular ? \"\" : \"\"}${obj.note}
`).join(\"\\n\") + \"
\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"

Parser Definition Parsers define parsers that acquire, analyze and act on code.

\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"

Atom Definition Parsers analyze the atoms in a line.

\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` A${category.replace(\"Phase\", \"\").substr(1)}Time.`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `` : \"\"\n const rssTag = rssFeedUrl ? `` : \"\"\n const gitTag = gitRepo ? `` : \"\"\n return `\n \n ${title}\n \n \n \n \n \n \n \n \n \n ${faviconTag}\n ${gitTag}\n ${rssTag}\n \n \n `\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `
${this.subparticlesToString()}
`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return ``\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAnyAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAnyAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `
${this.buildTxt()}
`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n \n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`).concat([\".scroll.css\"])\n }\n buildHtml() {\n return this.files.map(name => ``).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType javascriptAnyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nscrollLinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cue link\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get linkAttribute() {\n return \"href\"\n }\n get attributes() {\n const attrs = [`${this.linkAttribute}=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nscrollClickParser\n extends scrollLinkParser\n description An a tag with an onclick.\n cue click\n javascript\n get linkAttribute() {\n return \"onclick\"\n }\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends scrollLinkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends scrollLinkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `${day} — ` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: ``, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom codeAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nscrollFileAddressParser\n catchAllAtomType filePathAtom\n catchAllParser scrollFileAddressParser\nchatLineParser\n popularity 0.009887\n catchAllAtomType stringAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser slashCommentParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {\n const result = {}\n const normalize = str => str.toLowerCase().trim()\n userColumnNames.forEach(userColumn => {\n // Strategy 1: Exact match\n const exactMatch = availableColumnNames.find(col => col === userColumn)\n if (exactMatch) {\n result[userColumn] = exactMatch\n return\n }\n // Strategy 2: Case-insensitive match\n const normalizedUserColumn = normalize(userColumn)\n const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)\n if (caseInsensitiveMatch) {\n result[userColumn] = caseInsensitiveMatch\n return\n }\n // Strategy 3: Levenshtein distance match\n const THRESHOLD = 2 // Consider matches with distance <= 2 as \"very close\"\n let bestMatch = null\n let bestDistance = Infinity\n availableColumnNames.forEach(col => {\n const distance = this.root.levenshteinDistance(userColumn, col)\n if (distance < bestDistance) {\n bestDistance = distance\n bestMatch = col\n }\n })\n // Only use Levenshtein match if it's very close\n if (bestDistance <= THRESHOLD) {\n result[userColumn] = bestMatch\n return\n }\n // Strategy 4: Fallback - use original unmatched name\n result[userColumn] = userColumn\n })\n return result\n }\n connectColumnName(name) {\n return this.connectColumnNames([name])[name]\n }\n getErrors() {\n const errors = super.getErrors()\n if (errors.length && this.previous.cue !== \"assertIgnoreBelowErrors\")\n return errors\n return []\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollParseDateParser\n extends abstractTableTransformParser\n description Parse dates in a column into standard format.\n cue parseDate\n atoms cueAtom columnNameAtom\n example\n sampleData stocks.csv\n parseDate date\n linechart\n x date\n y price\n javascript\n get coreTable() {\n const columnName = this.connectColumnName(this.getAtom(1))\n const formatOut = this.get(\"format\") || \"YYYY-MM-DD\"\n const {dayjs} = this.root\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const value = row[columnName]\n if (value) {\n const date = dayjs(value)\n if (date.isValid())\n newRow[columnName] = date.format(formatOut)\n }\n } catch (err) {\n console.error(`Error parsing date in column ${columnName}:`, err)\n }\n return newRow\n })\n }\n formatParser\n description Specify output date format\n atoms cueAtom stringAtom\n cueFromId\n single\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n reduceParser\n description Specify how to aggregate a column when grouping data\n atoms cueAtom columnNameAtom reductionTypeAtom newColumnNameAtom\n cue reduce\n example\n data.csv\n groupBy year\n reduce score sum totalScore\n reduce name concat names\n printTable\n javascript\n get reduction() {\n return {\n source: this.getAtom(1),\n reduction: this.getAtom(2),\n name: this.getAtom(3) || this.getAtomsFrom(1).join(\"_\")\n }\n }\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(particle => particle.reduction)\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n const newColName = col.name\n if (reduction === \"concat\") {\n newRow[newColName] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[newColName] = group.find((row) => row[sourceColName] !== \"\")?.[sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[newColName] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom\n catchAllAtomType constantAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.connectColumnName(this.getAtom(1))\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || (untypedScalarValue === undefined && !operator.includes(\"mpty\"))) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return typedAtom === \"\" || typedAtom === undefined\n else if (operator === \"notEmpty\") return typedAtom !== \"\" && typedAtom !== undefined\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n if (!this._columnNames) {\n const names = this.getAtomsFrom(1)\n this._columnNamesMap = this.connectColumnNames(names)\n this._columnNames = names.map(name => this._columnNamesMap[name])\n }\n return this._columnNames\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType codeAtom\n cue compose\n atoms cueAtom newColumnNameAtom\n example\n table\n compose sentence My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n atoms cueAtom\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`${name}`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.connectColumnName(this.getAtom(1))\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => this.connectColumnName(col.replace(/^\\-/, \"\"))), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nassertRowCountParser\n extends abstractTableTransformParser\n description Test row count is expected value.\n atoms cueAtom integerAtom\n cueFromId\n javascript\n getErrors() {\n const errors = super.getErrors()\n const actualRows = this.coreTable.length\n const expectedRows = parseInt(this.content)\n if (actualRows !== expectedRows)\n return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))\n return errors\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom newColumnNameAtom\n catchAllAtomType newColumnNameAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nscrollSummarizeParser\n extends abstractTableTransformParser\n description Generate summary statistics for each column.\n cue summarize\n example\n table data.csv\n summarize\n printTable\n javascript\n get coreTable() {\n const {lodash} = this.root\n const sourceData = this.parent.coreTable\n if (!sourceData.length) return []\n return this.parent.columnNames.map(colName => {\n const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)\n const numericValues = values.filter(val => typeof val === \"number\" && !isNaN(val))\n const sorted = [...numericValues].sort((a, b) => a - b)\n // Calculate mode\n const frequency = {}\n values.forEach(val => {\n frequency[val] = (frequency[val] || 0) + 1\n })\n const mode = Object.entries(frequency)\n .sort((a, b) => b[1] - a[1])\n .map(entry => entry[0])[0]\n // Calculate median for numeric values\n const median = sorted.length ? \n sorted.length % 2 === 0 \n ? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2\n : sorted[Math.floor(sorted.length/2)]\n : null\n const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null\n const theType = typeof values[0]\n const count = values.length\n const mean = theType === \"number\" ? sum/count : \"\"\n return {\n name: colName,\n type: theType,\n incompleteCount: sourceData.length - values.length,\n uniqueCount: new Set(values).size,\n count,\n sum,\n median,\n mean,\n min: sorted.length ? sorted[0] : null,\n max: sorted.length ? sorted[sorted.length - 1] : null,\n mode\n }\n })\n }\n get columnNames() {\n return [\"name\", \"type\", \"incompleteCount\", \"uniqueCount\", \"count\", \"sum\", \"median\", \"mean\", \"min\", \"max\", \"mode\"]\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType javascriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType stringAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollStrokeParser\n cue stroke\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollSortParser\n cue sort\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nabstractPlotLabelParser\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\nquoteLineParser\n popularity 0.004172\n catchAllAtomType stringAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n try {\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n } catch (err) {\n console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)\n console.error(err)\n return \"\"\n }\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `Edit`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"164.12.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particleethodName])\n .map((particle, index) => particleethodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `\\n\\n${bodyTag}${content}\\n\\n` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n get d3() {\n return this.isNodeJs() ? require('d3') : d3\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll(options = {}) {\n await this.load()\n await this.buildOne(options)\n await this.buildTwo(options)\n }\n async buildOne(options) {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne(options)\n }\n }\n async buildTwo(options) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(options)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n levenshteinDistance(a, b) {\n const m = a.length\n const n = b.length\n const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))\n for (let i = 0; i <= m; i++) {\n dp[i][0] = i\n }\n for (let j = 0; j <= n; j++) {\n dp[0][j] = j\n }\n for (let i = 1; i <= m; i++) {\n for (let j = 1; j <= n; j++) {\n const cost = a[i - 1] === b[j - 1] ? 0 : 1\n dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)\n }\n }\n return dp][n]\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concepteasure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) roweasureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else roweasureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCacheeasureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCacheeasureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCacheeasureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` \n ${title}\n ${canonicalUrl}\n ${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}\n `\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType codeAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n atoms cueAtom\n description Table from inline delimited data.\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser\nBlobParser\n baseParser blobParser"}
Breck Yunits
Breck Yunits
16 days ago
package.json
Changed around line 1
- "version": "0.72.0",
+ "version": "0.73.0",
public/edit.scroll
Changed around line 30: libs.js
- div#gitClone.greyText
+ div#folderLinks.greyText
public/releaseNotes.scroll
Changed around line 18: node_modules/scroll-cli/microlangs/changes.parsers
+ 📦 0.73.0 1/5/2025
+ 🎉 various improvements and fixes
+ 🏥 mobile layout fixes
+
public/scrollHubEditor.js
Changed around line 36: class EditorApp {
- return Math.max(300, window.innerHeight - 200)
+ return this.isMobile ? 400 : Math.max(300, window.innerHeight - 200)
- const { editorHeight } = this
- this.codeMirrorInstance.setSize(this.width, editorHeight)
+ const { editorHeight, isMobile } = this
+ const fileListHeight = isMobile ? "auto" : `${editorHeight - 138}px`
+ const fileListWidth = isMobile ? document.body.clientWidth - 40 + "px" : "200px"
- fileList.style.height = `${editorHeight - 138}px`
+ fileList.style.height = fileListHeight
+ fileList.style.width = fileListWidth
+ this.codeMirrorInstance.setSize(this.width, editorHeight)
Changed around line 56: class EditorApp {
+ get isMobile() {
+ return window.innerWidth < 768
+ }
+
+ if (this.isMobile) return bodyWidth - 40
Changed around line 812: Follow me on X or GitHub
- document.getElementById("gitClone").innerHTML =
+ document.getElementById("folderLinks").innerHTML =
public/scrollHubStyle.css
Changed around line 74: body {
- white-space: pre;
Changed around line 128: body {
- max-width: 200px;
+ width: 200px;
Breck Yunits
Breck Yunits
17 days ago
ScrollHub.js
Changed around line 515: If you'd like to create this folder, visit our main site to get started.
- const base = this.getBaseUrlForFolder(folderName, req.hostname, req.protocol, this.isLocalHost)
+ const base = this.getBaseUrlForFolder(folderName, req.hostname, req.protocol + ":", this.isLocalHost)
Breck Yunits
Breck Yunits
17 days ago
FolderIndex.js
Changed around line 3: const { spawn } = require("child_process")
- const getBaseUrlForFolder = (folderName, hostname, protocol, isLocalHost) => {
- // if localhost, no custom domains
- if (isLocalHost) return `/${folderName}`
-
- if (!folderName.includes(".")) return protocol + "//" + hostname + "/" + folderName
-
- // now it might be a custom domain, serve it as if it is
- // of course, sometimes it would not be
- return protocol + "//" + folderName
- }
-
Changed around line 126: class FolderIndex {
- const folderLink = getBaseUrlForFolder(folder, scrollHub.hostname, hasSslCert ? "https:" : "http:", scrollHub.isLocalHost)
+ const folderLink = scrollHub.getBaseUrlForFolder(folder, scrollHub.hostname, hasSslCert ? "https:" : "http:", scrollHub.isLocalHost)
ScrollHub.js
Changed around line 349: class ScrollHub {
+ getBaseUrlForFolder(folderName, hostname, protocol, isLocalHost) {
+ // if localhost, no custom domains
+ if (isLocalHost) return `/${folderName}`
+
+ if (!folderName.includes(".")) return protocol + "//" + hostname + "/" + folderName
+
+ // now it might be a custom domain, serve it as if it is
+ // of course, sometimes it would not be
+ return protocol + "//" + folderName
+ }
+
Changed around line 515: If you'd like to create this folder, visit our main site to get started.
- return res.redirect(folderName + "/.requests.html")
+ const base = this.getBaseUrlForFolder(folderName, req.hostname, req.protocol, this.isLocalHost)
+ return res.redirect(base + "/.requests.html")
Breck Yunits
Breck Yunits
17 days ago
open commits and traffic in modal
FolderIndex.js
Changed around line 1
+ const AnsiToHtml = require("ansi-to-html")
Changed around line 162: class FolderIndex {
+
+ async getCommits(folderName, count) {
+ const { scrollHub } = this
+ const { rootFolder } = scrollHub
+ const folderPath = path.join(rootFolder, folderName)
+
+ try {
+ // Check if there are any commits
+ const commitCountData = await new Promise((resolve, reject) => {
+ const gitRevListProcess = spawn("git", ["rev-list", "--count", "HEAD"], { cwd: folderPath })
+ let data = ""
+
+ gitRevListProcess.stdout.on("data", chunk => {
+ data += chunk.toString()
+ })
+
+ gitRevListProcess.on("close", code => {
+ if (code === 0) {
+ resolve(data.trim())
+ } else {
+ reject(new Error("Failed to get commit count"))
+ }
+ })
+ })
+
+ const numCommits = parseInt(commitCountData, 10)
+ if (numCommits === 0) {
+ return []
+ }
+
+ // Get detailed commit information with a specific format
+ const logOutput = await new Promise((resolve, reject) => {
+ const gitLogProcess = spawn("git", ["log", `-${count}`, "--color=always", "--date=iso", "--format=commit %H%nAuthor: %an <%ae>%nDate: %ad%nSubject: %s%n%n%b%n", "-p"], { cwd: folderPath })
+
+ let output = ""
+ gitLogProcess.stdout.on("data", data => {
+ output += data.toString()
+ })
+
+ gitLogProcess.on("close", code => {
+ if (code === 0) {
+ resolve(output)
+ } else {
+ reject(new Error("Failed to get commit information"))
+ }
+ })
+ })
+
+ // Parse the git log output into structured data
+ const commits = []
+ const commitChunks = logOutput.split(/(?=commit [0-9a-f]{40}\n)/)
+
+ for (const chunk of commitChunks) {
+ if (!chunk.trim()) continue
+
+ const commitMatch = chunk.match(/commit (\w+)\n/)
+ const authorMatch = chunk.match(/Author: ([^<]+)<([^>]+)>/)
+ const dateMatch = chunk.match(/Date:\s+(.+)\n/)
+ const messageMatch = chunk.match(/\n\n\s+(.+?)\n/)
+ const diffContent = chunk.split(/\n\n/)[2] || ""
+
+ if (commitMatch && authorMatch && dateMatch) {
+ commits.push({
+ id: commitMatch[1],
+ name: authorMatch[1].trim(),
+ email: authorMatch[2].trim(),
+ time: new Date(dateMatch[1]),
+ message: messageMatch ? messageMatch[1].trim() : "",
+ diff: diffContent,
+ rawOutput: chunk // Keep raw output for HTML generation
+ })
+ }
+ }
+
+ return commits
+ } catch (error) {
+ console.error(`Error in getCommits: ${error.message}`)
+ throw error
+ }
+ }
+
+ async sendCommits(folderName, count, res) {
+ try {
+ const commits = await this.getCommits(folderName, count)
+
+ if (commits.length === 0) {
+ res.status(200).send(`No commits available for ${folderName}.`)
+ return
+ }
+
+ const convert = new AnsiToHtml({ escapeXML: true })
+
+ // Send HTML header
+ res.setHeader("Content-Type", "text/html; charset=utf-8")
+ res.write(`
+
+
+
+
+
+ Last ${count} Commits for ${folderName}
+
+
+
+ `)
+
+ // Process each commit
+ for (const commit of commits) {
+ let output = commit.rawOutput
+ // Convert ANSI color codes to HTML
+ output = convert.toHtml(output)
+ // Add restore version button
+ output = output.replace(/(commit\s)([0-9a-f]{40})/, (match, prefix, hash) => {
+ return `
+ ${prefix}${hash}
+
+
+
+ `
+ })
+ res.write(output + "
\n")
+ }
+
+ res.end("")
+ } catch (error) {
+ console.error(`Error in sendCommits: ${error.message}`)
+ res.status(500).send("An error occurred while fetching the git log")
+ }
+ }
ScrollHub.js
Changed around line 15: const compression = require("compression")
- const AnsiToHtml = require("ansi-to-html")
Changed around line 596: If you'd like to create this folder, visit our main site to get started.
- const { app, rootFolder, folderCache } = this
+ const { app, rootFolder, folderCache, folderIndex } = this
Changed around line 614: If you'd like to create this folder, visit our main site to get started.
- app.get("/diffs.htm/:folderName", async (req, res) => {
- const folderName = req.params.folderName
- const folderPath = path.join(rootFolder, folderName)
+ app.get("/commits.htm", async (req, res) => {
+ const folderName = this.getFolderName(req)
- const count = req.query.count || 10
-
- try {
- // Check if there are any commits
- const gitRevListProcess = spawn("git", ["rev-list", "--count", "HEAD"], { cwd: folderPath })
- let commitCountData = ""
-
- gitRevListProcess.stdout.on("data", data => {
- commitCountData += data.toString()
- })
-
- gitRevListProcess.stderr.on("data", data => {
- console.error(`git rev-list stderr: ${data}`)
- })
-
- gitRevListProcess.on("close", code => {
- if (code !== 0) {
- res.status(500).send("An error occurred while checking commit count")
- return
- }
-
- const numCommits = parseInt(commitCountData.trim(), 10)
- if (numCommits === 0) {
- res.status(200).send("No commits available.")
- return
- }
-
- // Now spawn git log process
- const gitLogProcess = spawn("git", ["log", "-p", `-${count}`, "--color=always"], { cwd: folderPath })
- const convert = new AnsiToHtml({ escapeXML: true })
-
- res.setHeader("Content-Type", "text/html; charset=utf-8")
- res.write(`
-
-
-
-
-
- Last 10 Commits for ${folderName}
-
-
-
- `)
-
- let buffer = ""
- gitLogProcess.stdout.on("data", data => {
- buffer += data.toString()
- // Process complete lines
- let lines = buffer.split("\n")
- buffer = lines.pop() // Keep incomplete line in buffer
-
- lines = lines.map(line => {
- // Convert ANSI to HTML
- line = convert.toHtml(line)
- // Replace commit hashes with forms
- line = line.replace(/(commit\s)([0-9a-f]{40})/, (match, prefix, hash) => {
- return `${"-".repeat(60)}
- ${prefix}${hash}
-
-
-
- `
- })
- return line
- })
-
- res.write(lines.join("\n") + "\n")
- })
-
- gitLogProcess.stderr.on("data", data => {
- console.error(`git log stderr: ${data}`)
- })
-
- gitLogProcess.on("close", code => {
- if (code !== 0) {
- console.error(`git log process exited with code ${code}`)
- res.status(500).end("An error occurred while fetching the git log")
- } else {
- // Process any remaining buffered data
- if (buffer.length > 0) {
- buffer = convert.toHtml(buffer)
- buffer = buffer.replace(/(commit\s)([0-9a-f]{40})/, (match, prefix, hash) => {
- return `${"-".repeat(60)}
- ${prefix}${hash}
-
-
-
- `
- })
- res.write(buffer)
- }
- res.end("")
- }
- })
- })
- } catch (error) {
- console.error(error)
- res.status(500).send("An error occurred while fetching the git log")
- }
+ await folderIndex.sendCommits(folderName, req.query.count || 10, res)
+ })
+ app.get("/commits.json", async (req, res) => {
+ const folderName = this.getFolderName(req)
+ if (!folderCache[folderName]) return res.status(404).send("Folder not found")
+ const commits = await folderIndex.getCommits(folderName, req.query.count || 10)
Changed around line 644: ${prefix}${hash}
- res.redirect("/diffs.htm/" + folderName)
+ res.redirect("/commits.htm?folderName=" + folderName)
Changed around line 1633: Download folders as JSON | CSV | TSV
- compose hashLink diffs.htm/{folder}
+ compose hashLink commits.htm?folderName={folder}
public/docs.scroll
Changed around line 60: class ScrollHub {
- `/:repo.git/*` - Git repository access
- `/revisions.htm/:folderName` - View commit history
- - `/diffs.htm/:folderName` - View file differences
+ - `/commits.htm?folderName=:folderName` - View file differences
- `/revert.htm/:folderName` - Restore previous versions
public/scrollHubEditor.js
Changed around line 480: class EditorApp {
- const keyStr = shortcut.key.replace("command", "cmd")
+ const keyStr = shortcut.key.startsWith("nokey") ? " " : shortcut.key.replace("command", "cmd")
Changed around line 548: command+1 previousFileCommand Navigation
- ctrl+w showWelcomeMessageCommand Help`
+ nokey1 showWelcomeMessageCommand Help`
Changed around line 567: ctrl+w showWelcomeMessageCommand Help`
- const { keyboardShortcuts } = this
+ const keyboardShortcuts = this.keyboardShortcuts.filter(key => !key.key.startsWith("nokey"))
Changed around line 788: Follow me on X or GitHub
+ openIframeModal(event) {
+ const url = event.currentTarget.href
+ const { folderName } = this
+ const modalContent = `
+
+ src="${url}"
+ style="width: 100%; height: 80vh; border: none;"
+ >
+ `
+
+ this.openModal(modalContent, url, event)
+ }
+
+ // Update the updateFooterLinks method to use the new modal
public/scrollHubStyle.css
Changed around line 337: textarea {
- background: var(--kbd-bg);
+ background: var(--primary-color);
Breck Yunits
Breck Yunits
19 days ago
public/scrollHubStyle.css
Changed around line 210: form {
+ padding: 0;
Breck Yunits
Breck Yunits
19 days ago
public/scrollHubEditor.js
Changed around line 49: class EditorApp {
- this.codeMirrorInstance.setSize(width, window.innerHeight - 40) // subtract 40 for padding
+ this.codeMirrorInstance.setSize(width, window.innerHeight) // subtract 40 for padding
public/scrollHubStyle.css
Changed around line 210: form {
- padding-top: 20px;
- padding-bottom: 20px;
Breck Yunits
Breck Yunits
19 days ago
public/scrollHubEditor.js
Changed around line 49: class EditorApp {
- this.codeMirrorInstance.setSize(width, window.innerHeight)
+ this.codeMirrorInstance.setSize(width, window.innerHeight - 40) // subtract 40 for padding
+ get width() {
+ const bodyWidth = document.body.clientWidth
+ const maxWidth = 784
+ let computedWidth = bodyWidth - 270
+ return Math.max(100, Math.min(maxWidth, computedWidth))
+ }
+
Changed around line 199: class EditorApp {
- get width() {
- const bodyWidth = document.body.clientWidth
- const maxWidth = 784
- let computedWidth = bodyWidth - 270
- return Math.max(100, Math.min(maxWidth, computedWidth))
- }
-
public/scrollHubStyle.css
Changed around line 211: form {
+ padding-bottom: 20px;
Breck Yunits
Breck Yunits
19 days ago
new file nav
public/scrollHubEditor.js
Changed around line 475: class EditorApp {
+ if (category === "Hidden") return ""
Changed around line 544: command+. toggleFocusModeCommand Editor
- alt+right nextFileCommand Navigation
- alt+left previousFileCommand Navigation
- command+/ toggleHelpCommand Help
+ command+1 previousFileCommand Navigation
+ command+2 nextFileCommand Navigation
+ command+/ toggleHelpCommand Hidden
Breck Yunits
Breck Yunits
19 days ago
Focus mode link
public/edit.scroll
Changed around line 24: libs.js
public/scrollHubEditor.js
Changed around line 81: class EditorApp {
+ if (!this.codeMirrorInstance.hasFocus()) this.focusOnEnd()
Changed around line 448: class EditorApp {
+
Sentences:${sentences.length}
-
Sentences:${sentences.length}
-
Reading Time:${readingTimeMinutes} min
+
Reading Time:${readingTimeMinutes} min
Changed around line 462: class EditorApp {
+ toggleMetricsCommand(event) {
+ if (this._openModal === "metrics") this.closeModal()
+ else this.openMetricsCommand(event)
+ }
+
Changed around line 542: command+p formatFileCommand File
- command+3 openMetricsCommand Editor
+ command+3 toggleMetricsCommand Editor
Changed around line 970: Follow me on X or GitHub
- const lastLine = lines.pop()
- if (lines.length < 24 && this._isFirstOpen) {
- // if its a small file, put user right in editing experience
- this.codeMirrorInstance.setCursor({ line: lines.length, ch: lastLine.length })
- this.codeMirrorInstance.focus()
- }
+ // if its a small file, put user right in editing experience
+ if (lines.length < 24 && this._isFirstOpen) this.focusOnEnd()
+
+ focusOnEnd() {
+ const lines = this.codeMirrorInstance.getValue().split("\n")
+ const lastLine = lines.pop()
+ this.codeMirrorInstance.setCursor({ line: lines.length, ch: lastLine.length })
+ this.codeMirrorInstance.focus()
+ }
+
public/scrollHubStyle.css
Changed around line 87: body {
- .renameFileLink {
+ .renameFileLink,
+ .focusLink {